Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 67a8f61af8bdd2583ef8ed24d6ad80d7e39ae677


Parents : 23e6b6e
Author : Mark Qvist <mark@unsigned.io>
Date : 2023-07-10T02:49:58+02:00

Updated build system for Kivy 2.2.1

Changes

126 files changed, 9739 insertions(+), 4051 deletions(-)

M .gitignore +2 -1
M sbapp/main.py +2 -2
M setup.py +2 -2

Diff

diff --git a/.gitignore b/.gitignore
index 241691de..cce6603d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+sbapp/kivymd_working
sbapp/.buildozer
sbapp/requirements.txt
sbapp/venv
@@ -29,4 +30,4 @@ build
dist
docs/build
sideband*.egg-info
-sbapp*.egg-info
\ No newline at end of file
+sbapp*.egg-info

diff --git a/sbapp/Makefile b/sbapp/Makefile
index 18ba2a38..da6841f9 100644
--- a/sbapp/Makefile
+++ b/sbapp/Makefile
@@ -30,12 +30,14 @@ patchsdl:
cp patches/PythonService.java .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/src/main/java/org/kivy/android/PythonService.java
injectxml:
+ # mkdir /home/markqvist/.local/lib/python3.11/site-packages/pythonforandroid/bootstraps/sdl2/build/src/main/xml
# Inject XML on arm64-v8a
mkdir -p .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/src/main/res/xml
mkdir -p .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/templates
cp patches/device_filter.xml .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/src/main/res/xml/
cp patches/file_paths.xml .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/src/main/res/xml/
cp patches/AndroidManifest.tmpl.xml .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/templates/
+ cp patches/p4a_build.py .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/build.py
debug:
buildozer android debug

diff --git a/sbapp/buildozer.spec b/sbapp/buildozer.spec
index f9f84835..6d762052 100644
--- a/sbapp/buildozer.spec
+++ b/sbapp/buildozer.spec
@@ -12,9 +12,10 @@ version.regex = __version__ = ['"](.*)['"]
version.filename = %(source.dir)s/main.py
android.numeric_version = 20230204
-requirements = python3==3.9.5,hostpython3==3.9.5,cryptography,cffi,pycparser,kivy==2.1.0,pygments,sdl2,sdl2_ttf==2.0.15,pillow,qrcode==7.3.1,netifaces,libbz2,pydenticon,usb4a,usbserial4a
+#requirements = python3==3.9.5,hostpython3==3.9.5,cryptography,cffi,pycparser,kivy==2.2.1,pygments,sdl2,sdl2_ttf==2.0.15,pillow,qrcode==7.3.1,netifaces,libbz2,pydenticon,usb4a,usbserial4a
+requirements = kivy==2.2.1,libbz2,pillow,qrcode==7.3.1,usb4a,usbserial4a
+
p4a.local_recipes = ../Others/python-for-android/pythonforandroid/recipes
-requirements.source.kivymd = ../../Others/KivyMD-master
icon.filename = %(source.dir)s/assets/icon.png
presplash.filename = %(source.dir)s/assets/presplash_small.png
@@ -27,7 +28,7 @@ fullscreen = 0
android.permissions = INTERNET,POST_NOTIFICATIONS,WAKE_LOCK,FOREGROUND_SERVICE,CHANGE_WIFI_MULTICAST_STATE,BLUETOOTH_CONNECT
android.api = 30
android.minapi = 24
-android.ndk = 23b
+android.ndk = 25b
android.skip_update = False
android.accept_sdk_license = True
android.release_artifact = apk

diff --git a/sbapp/kivymd/__init__.py b/sbapp/kivymd/__init__.py
index fa6cb272..bb05e0f6 100644
--- a/sbapp/kivymd/__init__.py
+++ b/sbapp/kivymd/__init__.py
@@ -26,11 +26,12 @@ import os
import kivy
from kivy.logger import Logger
-__version__ = "1.1.0.dev0"
+__version__ = "1.2.0.dev0"
"""KivyMD version."""
release = False
-kivy.require("2.0.0")
+if "READTHEDOCS" not in os.environ:
+ kivy.require("2.2.0")
try:
from kivymd._version import __date__, __hash__, __short_hash__
@@ -49,9 +50,6 @@ images_path = os.path.join(path, f"images{os.sep}")
uix_path = os.path.join(path, "uix")
"""Path to uix directory."""
-glsl_path = os.path.join(path, "data", "glsl")
-"""Path to glsl directory."""
-
_log_message = (
"KivyMD:"
+ (" Release" if release else "")

diff --git a/sbapp/kivymd/app.py b/sbapp/kivymd/app.py
index dfe3b274..bdad6883 100644
--- a/sbapp/kivymd/app.py
+++ b/sbapp/kivymd/app.py
@@ -54,7 +54,7 @@ from kivymd.theming import ThemeManager
class FpsMonitoring:
"""Implements a monitor to display the current FPS in the toolbar."""
- def fps_monitor_start(self) -> None:
+ def fps_monitor_start(self, anchor: str = "top") -> None:
"""Adds a monitor to the main application window."""
def add_monitor(*args):
@@ -62,7 +62,7 @@ class FpsMonitoring:
from kivymd.utils.fpsmonitor import FpsMonitor
- monitor = FpsMonitor()
+ monitor = FpsMonitor(anchor=anchor)
monitor.start()
Window.add_widget(monitor)

diff --git a/sbapp/kivymd/data/glsl/elevation/elevation.frag b/sbapp/kivymd/data/glsl/elevation/elevation.frag
deleted file mode 100644
index 03f042ac..00000000
--- a/sbapp/kivymd/data/glsl/elevation/elevation.frag
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
-The shader code has been refactored for the KivyMD library.
-You can find the original code of this shaders at the links:
-
-https://www.shadertoy.com/view/WtdSDs
-https://www.shadertoy.com/view/fsdyzB
-
-Additional thanks to iq for optimizing conditional block for individual
-corner radius:
-https://iquilezles.org/articles/distfunctions
-*/
-
-// For lower opengl version
-
-float custom_smoothstep(float a, float b, float x) {
- float t = clamp((x - a) / (b - a), 0.0, 1.0);
- return t * t * (3.0 - 2.0 * t);
-}
-
-float roundedBoxSDF(vec2 centerPosition, vec2 size, vec4 radius) {
- radius.xy = (centerPosition.x > 0.0) ? radius.xy : radius.zw;
- radius.x = (centerPosition.y > 0.0) ? radius.x : radius.y;
-
- vec2 q = abs(centerPosition) - (size - shadow_softness) + radius.x;
- return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius.x;
-}
-
-void mainImage(out vec4 fragColor, in vec2 fragCoord) {
- // Smooth the result (free antialiasing).
- float edge0 = 0.0;
- float smoothedAlpha = 1.0 - custom_smoothstep(0.0, edge0, 1.0);
- // Get the resultant shape.
- vec4 quadColor = mix(
- vec4(
- shadow_color[0],
- shadow_color[1],
- shadow_color[2],
- 0.0
- ),
- shadow_color,
- smoothedAlpha
- );
- // Apply a drop shadow effect.
- float shadowDistance = roundedBoxSDF(
- fragCoord.xy - mouse.xy - (size / 2.0), size / 2.0, shadow_radius
- );
- float shadowAlpha = 1.0 - custom_smoothstep(
- -shadow_softness, shadow_softness, shadowDistance
- );
- fragColor = mix(quadColor, shadow_color, shadowAlpha - smoothedAlpha);
-}

diff --git a/sbapp/kivymd/data/glsl/elevation/header.frag b/sbapp/kivymd/data/glsl/elevation/header.frag
deleted file mode 100644
index c09ce5c7..00000000
--- a/sbapp/kivymd/data/glsl/elevation/header.frag
+++ /dev/null
@@ -1,10 +0,0 @@
-#ifdef GL_FRAGMENT_PRECISION_HIGH
- precision highp float;
-#endif
-
-uniform vec4 resolution;
-uniform vec4 mouse;
-uniform vec2 size;
-uniform vec4 shadow_radius;
-uniform float shadow_softness;
-uniform vec4 shadow_color;

diff --git a/sbapp/kivymd/data/glsl/elevation/main.frag b/sbapp/kivymd/data/glsl/elevation/main.frag
deleted file mode 100644
index d9b87129..00000000
--- a/sbapp/kivymd/data/glsl/elevation/main.frag
+++ /dev/null
@@ -1,10 +0,0 @@
-vec2 gfc(in vec4 fc) {
- vec2 canvas_pos = resolution.zw;
- vec2 uv = fc.xy;
- uv.y -= canvas_pos.y;
- return uv;
-}
-
-void main(void) {
- mainImage(gl_FragColor, gfc(gl_FragCoord));
-}

diff --git a/sbapp/kivymd/effects/fadingedge/fadingedge.py b/sbapp/kivymd/effects/fadingedge/fadingedge.py
index 1af776a7..106d3c8f 100644
--- a/sbapp/kivymd/effects/fadingedge/fadingedge.py
+++ b/sbapp/kivymd/effects/fadingedge/fadingedge.py
@@ -164,6 +164,9 @@ class FadingEdgeEffect(ThemableBehavior):
index,
),
)
+ self.update_canvas(
+ self, self.size, rectangle_top, rectangle_bottom, i
+ )
def update_canvas(
self,

diff --git a/sbapp/kivymd/factory_registers.py b/sbapp/kivymd/factory_registers.py
index 1a20b01a..4497bfe5 100644
--- a/sbapp/kivymd/factory_registers.py
+++ b/sbapp/kivymd/factory_registers.py
@@ -5,6 +5,8 @@ Register KivyMD widgets to use without import.
from kivy.factory import Factory
register = Factory.register
+register("MDSegmentedButton", module="kivymd.uix.segmentedbutton")
+register("MDSegmentedButtonItem", module="kivymd.uix.segmentedbutton")
register("MDScrollView", module="kivymd.uix.scrollview")
register("MDRecycleView", module="kivymd.uix.recycleview")
register("MDResponsiveLayout", module="kivymd.uix.responsivelayout")
@@ -37,6 +39,7 @@ register("FitImage", module="kivymd.uix.fitimage")
register("MDBackdrop", module="kivymd.uix.backdrop")
register("MDBanner", module="kivymd.uix.banner")
register("MDTooltip", module="kivymd.uix.tooltip")
+register("MDBottomSheet", module="kivymd.uix.bottomsheet")
register("MDBottomNavigation", module="kivymd.uix.bottomnavigation")
register("MDBottomNavigationItem", module="kivymd.uix.bottomnavigation")
register("MDToggleButton", module="kivymd.uix.behaviors.toggle_behavior")

diff --git a/sbapp/kivymd/fonts/materialdesignicons-webfont.ttf b/sbapp/kivymd/fonts/materialdesignicons-webfont.ttf
index d6855108..53061f16 100644
Binary files a/sbapp/kivymd/fonts/materialdesignicons-webfont.ttf and b/sbapp/kivymd/fonts/materialdesignicons-webfont.ttf differ

diff --git a/sbapp/kivymd/icon_definitions.py b/sbapp/kivymd/icon_definitions.py
index 3941eae2..0b9425a9 100755
--- a/sbapp/kivymd/icon_definitions.py
+++ b/sbapp/kivymd/icon_definitions.py
@@ -12,7 +12,7 @@ Themes/Icon Definitions
List of icons from materialdesignicons.com. These expanded material design
icons are maintained by Austin Andrews (Templarian on Github).
-LAST UPDATED: Version 7.0.96
+LAST UPDATED: Version 7.1.96
To preview the icons and their names, you can use the following application:
----------------------------------------------------------------------------
@@ -242,6 +242,8 @@ md_icons = {
"account-switch-outline": "\U000F04CB",
"account-sync": "\U000F191B",
"account-sync-outline": "\U000F191C",
+ "account-tag": "\U000F1C1B",
+ "account-tag-outline": "\U000F1C1C",
"account-tie": "\U000F0CE3",
"account-tie-hat": "\U000F1898",
"account-tie-hat-outline": "\U000F1899",
@@ -774,6 +776,7 @@ md_icons = {
"audio-video": "\U000F093D",
"audio-video-off": "\U000F11B6",
"augmented-reality": "\U000F0850",
+ "aurora": "\U000F1BB9",
"auto-download": "\U000F137E",
"auto-fix": "\U000F0068",
"auto-upload": "\U000F0069",
@@ -852,6 +855,8 @@ md_icons = {
"bandage": "\U000F0DAF",
"bank": "\U000F0070",
"bank-check": "\U000F1655",
+ "bank-circle": "\U000F1C03",
+ "bank-circle-outline": "\U000F1C04",
"bank-minus": "\U000F0DB0",
"bank-off": "\U000F1656",
"bank-off-outline": "\U000F1657",
@@ -1443,6 +1448,8 @@ md_icons = {
"camera-image": "\U000F08CC",
"camera-iris": "\U000F0104",
"camera-lock": "\U000F1A14",
+ "camera-lock-open": "\U000F1C0D",
+ "camera-lock-open-outline": "\U000F1C0E",
"camera-lock-outline": "\U000F1A15",
"camera-marker": "\U000F19A7",
"camera-marker-outline": "\U000F19A8",
@@ -1707,6 +1714,7 @@ md_icons = {
"chart-multiline": "\U000F08D4",
"chart-multiple": "\U000F1213",
"chart-pie": "\U000F012B",
+ "chart-pie-outline": "\U000F1BDF",
"chart-ppf": "\U000F1380",
"chart-sankey": "\U000F11DF",
"chart-sankey-variant": "\U000F11E0",
@@ -1978,22 +1986,53 @@ md_icons = {
"closed-caption-outline": "\U000F0DBD",
"cloud": "\U000F015F",
"cloud-alert": "\U000F09E0",
+ "cloud-alert-outline": "\U000F1BE0",
+ "cloud-arrow-down": "\U000F1BE1",
+ "cloud-arrow-down-outline": "\U000F1BE2",
+ "cloud-arrow-left": "\U000F1BE3",
+ "cloud-arrow-left-outline": "\U000F1BE4",
+ "cloud-arrow-right": "\U000F1BE5",
+ "cloud-arrow-right-outline": "\U000F1BE6",
+ "cloud-arrow-up": "\U000F1BE7",
+ "cloud-arrow-up-outline": "\U000F1BE8",
"cloud-braces": "\U000F07B5",
- "cloud-check": "\U000F0160",
- "cloud-check-outline": "\U000F12CC",
+ "cloud-cancel": "\U000F1BE9",
+ "cloud-cancel-outline": "\U000F1BEA",
+ "cloud-check": "\U000F1BEB",
+ "cloud-check-outline": "\U000F1BEC",
+ "cloud-check-variant": "\U000F0160",
+ "cloud-check-variant-outline": "\U000F12CC",
"cloud-circle": "\U000F0161",
+ "cloud-circle-outline": "\U000F1BED",
+ "cloud-clock": "\U000F1BEE",
+ "cloud-clock-outline": "\U000F1BEF",
+ "cloud-cog": "\U000F1BF0",
+ "cloud-cog-outline": "\U000F1BF1",
"cloud-download": "\U000F0162",
"cloud-download-outline": "\U000F0B7D",
"cloud-lock": "\U000F11F1",
+ "cloud-lock-open": "\U000F1BF2",
+ "cloud-lock-open-outline": "\U000F1BF3",
"cloud-lock-outline": "\U000F11F2",
+ "cloud-minus": "\U000F1BF4",
+ "cloud-minus-outline": "\U000F1BF5",
+ "cloud-off": "\U000F1BF6",
"cloud-off-outline": "\U000F0164",
"cloud-outline": "\U000F0163",
"cloud-percent": "\U000F1A35",
"cloud-percent-outline": "\U000F1A36",
+ "cloud-plus": "\U000F1BF7",
+ "cloud-plus-outline": "\U000F1BF8",
"cloud-print": "\U000F0165",
"cloud-print-outline": "\U000F0166",
"cloud-question": "\U000F0A39",
- "cloud-refresh": "\U000F052A",
+ "cloud-question-outline": "\U000F1BF9",
+ "cloud-refresh": "\U000F1BFA",
+ "cloud-refresh-outline": "\U000F1BFB",
+ "cloud-refresh-variant": "\U000F052A",
+ "cloud-refresh-variant-outline": "\U000F1BFC",
+ "cloud-remove": "\U000F1BFD",
+ "cloud-remove-outline": "\U000F1BFE",
"cloud-search": "\U000F0956",
"cloud-search-outline": "\U000F0957",
"cloud-sync": "\U000F063F",
@@ -2311,6 +2350,7 @@ md_icons = {
"currency-rub": "\U000F01B1",
"currency-rupee": "\U000F1976",
"currency-sign": "\U000F07BE",
+ "currency-thb": "\U000F1C05",
"currency-try": "\U000F01B2",
"currency-twd": "\U000F07BF",
"currency-uah": "\U000F1B9B",
@@ -2750,6 +2790,10 @@ md_icons = {
"eye-check-outline": "\U000F0D05",
"eye-circle": "\U000F0B94",
"eye-circle-outline": "\U000F0B95",
+ "eye-lock": "\U000F1C06",
+ "eye-lock-open": "\U000F1C07",
+ "eye-lock-open-outline": "\U000F1C08",
+ "eye-lock-outline": "\U000F1C09",
"eye-minus": "\U000F1026",
"eye-minus-outline": "\U000F1027",
"eye-off": "\U000F0209",
@@ -2858,6 +2902,8 @@ md_icons = {
"file-document": "\U000F0219",
"file-document-alert": "\U000F1A97",
"file-document-alert-outline": "\U000F1A98",
+ "file-document-arrow-right": "\U000F1C0F",
+ "file-document-arrow-right-outline": "\U000F1C10",
"file-document-check": "\U000F1A99",
"file-document-check-outline": "\U000F1A9A",
"file-document-edit": "\U000F0DC8",
@@ -3731,6 +3777,9 @@ md_icons = {
"helicopter": "\U000F0AC2",
"help": "\U000F02D6",
"help-box": "\U000F078B",
+ "help-box-multiple": "\U000F1C0A",
+ "help-box-multiple-outline": "\U000F1C0B",
+ "help-box-outline": "\U000F1C0C",
"help-circle": "\U000F02D7",
"help-circle-outline": "\U000F0625",
"help-network": "\U000F06F5",
@@ -3907,6 +3956,7 @@ md_icons = {
"image-filter-center-focus-strong-outline": "\U000F0F00",
"image-filter-center-focus-weak": "\U000F02F2",
"image-filter-drama": "\U000F02F3",
+ "image-filter-drama-outline": "\U000F1BFF",
"image-filter-frames": "\U000F02F4",
"image-filter-hdr": "\U000F02F5",
"image-filter-none": "\U000F02F6",
@@ -4021,6 +4071,7 @@ md_icons = {
"keyboard-backspace": "\U000F030D",
"keyboard-caps": "\U000F030E",
"keyboard-close": "\U000F030F",
+ "keyboard-close-outline": "\U000F1C00",
"keyboard-esc": "\U000F12B7",
"keyboard-f1": "\U000F12AB",
"keyboard-f10": "\U000F12B4",
@@ -4263,6 +4314,12 @@ md_icons = {
"lock-open-variant-outline": "\U000F0FC7",
"lock-outline": "\U000F0341",
"lock-pattern": "\U000F06EA",
+ "lock-percent": "\U000F1C12",
+ "lock-percent-open": "\U000F1C13",
+ "lock-percent-open-outline": "\U000F1C14",
+ "lock-percent-open-variant": "\U000F1C15",
+ "lock-percent-open-variant-outline": "\U000F1C16",
+ "lock-percent-outline": "\U000F1C17",
"lock-plus": "\U000F05FB",
"lock-plus-outline": "\U000F16B2",
"lock-question": "\U000F08EF",
@@ -5072,6 +5129,7 @@ md_icons = {
"pencil-remove": "\U000F0DED",
"pencil-remove-outline": "\U000F0DEE",
"pencil-ruler": "\U000F1353",
+ "pencil-ruler-outline": "\U000F1C11",
"penguin": "\U000F0EC0",
"pentagon": "\U000F0701",
"pentagon-outline": "\U000F0700",
@@ -5309,6 +5367,41 @@ md_icons = {
"printer-off-outline": "\U000F1785",
"printer-outline": "\U000F1786",
"printer-pos": "\U000F1057",
+ "printer-pos-alert": "\U000F1BBC",
+ "printer-pos-alert-outline": "\U000F1BBD",
+ "printer-pos-cancel": "\U000F1BBE",
+ "printer-pos-cancel-outline": "\U000F1BBF",
+ "printer-pos-check": "\U000F1BC0",
+ "printer-pos-check-outline": "\U000F1BC1",
+ "printer-pos-cog": "\U000F1BC2",
+ "printer-pos-cog-outline": "\U000F1BC3",
+ "printer-pos-edit": "\U000F1BC4",
+ "printer-pos-edit-outline": "\U000F1BC5",
+ "printer-pos-minus": "\U000F1BC6",
+ "printer-pos-minus-outline": "\U000F1BC7",
+ "printer-pos-network": "\U000F1BC8",
+ "printer-pos-network-outline": "\U000F1BC9",
+ "printer-pos-off": "\U000F1BCA",
+ "printer-pos-off-outline": "\U000F1BCB",
+ "printer-pos-outline": "\U000F1BCC",
+ "printer-pos-pause": "\U000F1BCD",
+ "printer-pos-pause-outline": "\U000F1BCE",
+ "printer-pos-play": "\U000F1BCF",
+ "printer-pos-play-outline": "\U000F1BD0",
+ "printer-pos-plus": "\U000F1BD1",
+ "printer-pos-plus-outline": "\U000F1BD2",
+ "printer-pos-refresh": "\U000F1BD3",
+ "printer-pos-refresh-outline": "\U000F1BD4",
+ "printer-pos-remove": "\U000F1BD5",
+ "printer-pos-remove-outline": "\U000F1BD6",
+ "printer-pos-star": "\U000F1BD7",
+ "printer-pos-star-outline": "\U000F1BD8",
+ "printer-pos-stop": "\U000F1BD9",
+ "printer-pos-stop-outline": "\U000F1BDA",
+ "printer-pos-sync": "\U000F1BDB",
+ "printer-pos-sync-outline": "\U000F1BDC",
+ "printer-pos-wrench": "\U000F1BDD",
+ "printer-pos-wrench-outline": "\U000F1BDE",
"printer-search": "\U000F1457",
"printer-settings": "\U000F0707",
"printer-wireless": "\U000F0A0B",
@@ -5497,7 +5590,10 @@ md_icons = {
"remote-off": "\U000F0EC4",
"remote-tv": "\U000F0EC5",
"remote-tv-off": "\U000F0EC6",
+ "rename": "\U000F1C18",
"rename-box": "\U000F0455",
+ "rename-box-outline": "\U000F1C19",
+ "rename-outline": "\U000F1C1A",
"reorder-horizontal": "\U000F0688",
"reorder-vertical": "\U000F0689",
"repeat": "\U000F0456",
@@ -5566,8 +5662,10 @@ md_icons = {
"robot-outline": "\U000F167A",
"robot-vacuum": "\U000F070D",
"robot-vacuum-alert": "\U000F1B5D",
+ "robot-vacuum-off": "\U000F1C01",
"robot-vacuum-variant": "\U000F0908",
"robot-vacuum-variant-alert": "\U000F1B5E",
+ "robot-vacuum-variant-off": "\U000F1C02",
"rocket": "\U000F0463",
"rocket-launch": "\U000F14DE",
"rocket-launch-outline": "\U000F14DF",
@@ -6605,6 +6703,8 @@ md_icons = {
"tooltip-outline": "\U000F0526",
"tooltip-plus": "\U000F0BD6",
"tooltip-plus-outline": "\U000F0527",
+ "tooltip-question": "\U000F1BBA",
+ "tooltip-question-outline": "\U000F1BBB",
"tooltip-remove": "\U000F1560",
"tooltip-remove-outline": "\U000F1561",
"tooltip-text": "\U000F0528",
@@ -7219,3 +7319,94 @@ md_icons = {
"zodiac-virgo": "\U000F0A88",
"blank": " ",
}
+
+
+if __name__ == "__main__":
+ from kivy.lang import Builder
+ from kivy.properties import StringProperty
+ from kivy.uix.screenmanager import Screen
+
+ from kivymd.app import MDApp
+ from kivymd.uix.list import OneLineIconListItem
+
+ Builder.load_string(
+ """
+#:import images_path kivymd.images_path
+
+
+<CustomOneLineIconListItem>
+
+ IconLeftWidget:
+ icon: root.icon
+
+
+<PreviousMDIcons>
+
+ MDBoxLayout:
+ orientation: 'vertical'
+ spacing: dp(10)
+ padding: dp(20)
+
+ MDBoxLayout:
+ adaptive_height: True
+
+ MDIconButton:
+ icon: 'magnify'
+
+ MDTextField:
+ id: search_field
+ hint_text: 'Search icon'
+ on_text: root.set_list_md_icons(self.text, True)
+
+ RecycleView:
+ id: rv
+ key_viewclass: 'viewclass'
+ key_size: 'height'
+
+ RecycleBoxLayout:
+ padding: dp(10)
+ default_size: None, dp(48)
+ default_size_hint: 1, None
+ size_hint_y: None
+ height: self.minimum_height
+ orientation: 'vertical'
+ """
+ )
+
+ class CustomOneLineIconListItem(OneLineIconListItem):
+ icon = StringProperty()
+
+ class PreviousMDIcons(Screen):
+ def set_list_md_icons(self, text="", search=False):
+ """Builds a list of icons for the screen MDIcons."""
+
+ def add_icon_item(name_icon):
+ self.ids.rv.data.append(
+ {
+ "viewclass": "CustomOneLineIconListItem",
+ "icon": name_icon,
+ "text": name_icon,
+ "callback": lambda x: x,
+ }
+ )
+
+ self.ids.rv.data = []
+ for name_icon in md_icons.keys():
+ if search:
+ if text in name_icon:
+ add_icon_item(name_icon)
+ else:
+ add_icon_item(name_icon)
+
+ class MainApp(MDApp):
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.screen = PreviousMDIcons()
+
+ def build(self):
+ return self.screen
+
+ def on_start(self):
+ self.screen.set_list_md_icons()
+
+ MainApp().run()

diff --git a/sbapp/kivymd/material_resources.py b/sbapp/kivymd/material_resources.py
index 0a1b8115..2eb66266 100755
--- a/sbapp/kivymd/material_resources.py
+++ b/sbapp/kivymd/material_resources.py
@@ -35,4 +35,30 @@ else:
PORTRAIT_TOOLBAR_HEIGHT = STANDARD_INCREMENT
LANDSCAPE_TOOLBAR_HEIGHT = STANDARD_INCREMENT
+# Elevation.
+SEGMENT_CONTROL_SEGMENT_SWITCH_ELEVATION = 1
+FILE_MANAGER_TOP_APP_BAR_ELEVATION = 1
+FLOATING_ACTION_BUTTON_M2_ELEVATION = 1
+FLOATING_ACTION_BUTTON_M3_ELEVATION = 0.5
+CARD_STYLE_ELEVATED_M3_ELEVATION = 0.5
+CARD_STYLE_OUTLINED_FILLED_M3_ELEVATION = 0
+DATA_TABLE_ELEVATION = 4
+DROP_DOWN_MENU_ELEVATION = 2
+TOP_APP_BAR_ELEVATION = 2
+SNACK_BAR_ELEVATION = 2
+
+# Shadow softness.
+RAISED_BUTTON_SOFTNESS = 4
+FLOATING_ACTION_BUTTON_M3_SOFTNESS = 0
+DATA_TABLE_SOFTNESS = 12
+DROP_DOWN_MENU_SOFTNESS = 6
+
+# Shadow offset.
+RAISED_BUTTON_OFFSET = (0, -2)
+FLOATING_ACTION_BUTTON_M2_OFFSET = (0, -1)
+FLOATING_ACTION_BUTTON_M3_OFFSET = (0, -2)
+DATA_TABLE_OFFSET = (0, -2)
+DROP_DOWN_MENU_OFFSET = (0, -2)
+SNACK_BAR_OFFSET = (0, -2)
+
TOUCH_TARGET_HEIGHT = dp(48)

diff --git a/sbapp/kivymd/tests/base_test.py b/sbapp/kivymd/tests/base_test.py
deleted file mode 100644
index 9fa52eff..00000000
--- a/sbapp/kivymd/tests/base_test.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from kivy.tests.common import GraphicUnitTest
-
-from kivymd.app import MDApp
-
-
-class BaseTest(GraphicUnitTest):
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.app = MDApp() # NOQA

diff --git a/sbapp/kivymd/tests/pyinstaller/test_pyinstaller_packaging.py b/sbapp/kivymd/tests/pyinstaller/test_pyinstaller_packaging.py
deleted file mode 100644
index 3b81fdfd..00000000
--- a/sbapp/kivymd/tests/pyinstaller/test_pyinstaller_packaging.py
+++ /dev/null
@@ -1,94 +0,0 @@
-"""
-PyInstaller freezing test
-=========================
-
-PyInstaller must package KivyMD apps correctly.
-"""
-
-import subprocess
-
-from PyInstaller import __main__ as pyi_main
-
-
-def test_datas(tmp_path) -> None:
- """Test fonts and images."""
-
- app_name = "userapp"
- workpath = tmp_path / "build"
- distpath = tmp_path / "dist"
- app = tmp_path / (app_name + ".py")
- app.write_text(
- """
-import os
-
-from kivy.core.text import LabelBase
-
-import kivymd
-
-fonts = os.listdir(kivymd.fonts_path)
-print(fonts)
-assert "Roboto-Regular.ttf" in fonts
-assert "materialdesignicons-webfont.ttf" in fonts
-print(LabelBase._fonts.keys())
-assert "Roboto" in LabelBase._fonts.keys() # NOQA
-assert "Icons" in LabelBase._fonts.keys() # NOQA
-
-images = os.listdir(kivymd.images_path)
-print(images)
-assert "logo" in images
-assert "alpha_layer.png" in images
-assert "black.png" in images
-assert "blue.png" in images
-assert "red.png" in images
-assert "green.png" in images
-assert "yellow.png" in images
-assert "folder.png" in images
-assert "transparent.png" in images
-"""
- )
- pyi_main.run(
- [
- "--workpath",
- str(workpath),
- "--distpath",
- str(distpath),
- "--specpath",
- str(tmp_path),
- str(app),
- ]
- )
- subprocess.run([str(distpath / app_name / app_name)], check=True)
-
-
-def test_widgets(tmp_path) -> None:
- """Test that all widgets are accesible."""
-
- app_name = "userapp"
- workpath = tmp_path / "build"
- distpath = tmp_path / "dist"
- app = tmp_path / (app_name + ".py")
- app.write_text(
- """
-import os
-
-import kivymd # NOQA
-__import__("kivymd.uix.label")
-__import__("kivymd.uix.button")
-__import__("kivymd.uix.list")
-__import__("kivymd.uix.navigationdrawer")
-
-print(os.listdir(os.path.dirname(kivymd.uix.__path__[0])))
-"""
- )
- pyi_main.run(
- [
- "--workpath",
- str(workpath),
- "--distpath",
- str(distpath),
- "--specpath",
- str(tmp_path),
- str(app),
- ]
- )
- subprocess.run([str(distpath / app_name / app_name)], check=True)

diff --git a/sbapp/kivymd/tests/test_app.py b/sbapp/kivymd/tests/test_app.py
deleted file mode 100644
index 076e9dda..00000000
--- a/sbapp/kivymd/tests/test_app.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from kivy import lang
-from kivy.clock import Clock
-from kivy.tests.common import GraphicUnitTest
-
-from kivymd.app import MDApp
-from kivymd.theming import ThemeManager
-
-
-class AppTest(GraphicUnitTest):
- def test_start_raw_app(self):
- lang._delayed_start = None
- a = MDApp()
- Clock.schedule_once(a.stop, 0.1)
- a.run()
-
- def test_theme_manager_existance(self):
- lang._delayed_start = None
- a = MDApp()
- Clock.schedule_once(a.stop, 0.1)
- a.run()
- assert isinstance(a.theme_cls, ThemeManager)

diff --git a/sbapp/kivymd/tests/test_backdrop.py b/sbapp/kivymd/tests/test_backdrop.py
deleted file mode 100644
index 85b1c857..00000000
--- a/sbapp/kivymd/tests/test_backdrop.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from kivymd.tests.base_test import BaseTest
-
-
-class BackdropTest(BaseTest):
- def test_backdrop_raw_app(self):
- from kivymd.uix.backdrop import MDBackdrop
- from kivymd.uix.backdrop.backdrop import (
- MDBackdropBackLayer,
- MDBackdropFrontLayer,
- )
- from kivymd.uix.screen import MDScreen
- from kivymd.uix.widget import MDWidget
-
- self.render(
- MDScreen(
- MDBackdrop(
- MDBackdropBackLayer(MDWidget()),
- MDBackdropFrontLayer(MDWidget()),
- id="backdrop",
- title="Example Backdrop",
- header_text="Menu:",
- )
- )
- )

diff --git a/sbapp/kivymd/tests/test_bottom_navigation.py b/sbapp/kivymd/tests/test_bottom_navigation.py
deleted file mode 100644
index 9b3467c8..00000000
--- a/sbapp/kivymd/tests/test_bottom_navigation.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from kivymd.tests.base_test import BaseTest
-
-
-class BottomNavigationTest(BaseTest):
- def test_bottom_navigation_m3_style_raw_app(self):
- from kivymd.uix.bottomnavigation import (
- MDBottomNavigation,
- MDBottomNavigationItem,
- )
- from kivymd.uix.screen import MDScreen
-
- self.app.theme_cls.material_style = "M3"
- self.render(
- MDScreen(
- MDBottomNavigation(
- MDBottomNavigationItem(
- name="screen 1",
- text="Mail",
- icon="gmail",
- ),
- MDBottomNavigationItem(
- name="screen 2",
- text="Twitter",
- icon="twitter",
- badge_icon="numeric-10",
- ),
- panel_color="#eeeaea",
- selected_color_background="#97ecf8",
- text_color_active="red",
- )
- )
- )

diff --git a/sbapp/kivymd/tests/test_card.py b/sbapp/kivymd/tests/test_card.py
deleted file mode 100644
index 81dd0ddd..00000000
--- a/sbapp/kivymd/tests/test_card.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from kivymd.tests.base_test import BaseTest
-
-
-class CardTest(BaseTest):
- def test_card_m3_style_raw_app(self):
- from kivymd.uix.behaviors import RoundedRectangularElevationBehavior
- from kivymd.uix.card import MDCard
- from kivymd.uix.screen import MDScreen
-
- class MD3Card(MDCard, RoundedRectangularElevationBehavior):
- pass
-
- self.app.theme_cls.material_style = "M3"
- self.render(
- MDScreen(
- MD3Card(
- size_hint=(None, None),
- pos_hint={"center_x": 0.5, "center_y": 0.5},
- size=("200dp", "100dp"),
- line_color=(0.2, 0.2, 0.2, 0.8),
- style="elevated",
- md_bg_color="lightblue",
- )
- )
- )

diff --git a/sbapp/kivymd/tests/test_chip.py b/sbapp/kivymd/tests/test_chip.py
deleted file mode 100644
index 6f30f67a..00000000
--- a/sbapp/kivymd/tests/test_chip.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from kivymd.tests.base_test import BaseTest
-
-
-class ChipTest(BaseTest):
- def test_chip_raw_app(self):
- from kivymd.uix.chip import MDChip
- from kivymd.uix.screen import MDScreen
-
- self.render(
- MDScreen(
- MDChip(
- text="Portland",
- pos_hint={"center_x": 0.5, "center_y": 0.5},
- )
- )
- )

diff --git a/sbapp/kivymd/tests/test_create_project.py b/sbapp/kivymd/tests/test_create_project.py
deleted file mode 100644
index 31265fa3..00000000
--- a/sbapp/kivymd/tests/test_create_project.py
+++ /dev/null
@@ -1,15 +0,0 @@
-def test_create_project():
- import os
-
- os.system(
- f"python3.10 -m kivymd.tools.patterns.create_project "
- f"MVC "
- f"{os.path.expanduser('~')} "
- f"TestProject "
- f"python3.10 "
- f"stable "
- f"--name_screen TestProjectScreen "
- f"--name_database restdb "
- f"--use_hotreload yes"
- )
- assert os.path.exists(os.path.join(os.path.expanduser("~"), "TestProject"))

diff --git a/sbapp/kivymd/tests/test_fitimage.py b/sbapp/kivymd/tests/test_fitimage.py
deleted file mode 100644
index de159de9..00000000
--- a/sbapp/kivymd/tests/test_fitimage.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from kivymd.tests.base_test import BaseTest
-
-
-class FitImageTest(BaseTest):
- def test_fitimage_raw_app(self):
- import os
-
- from kivymd import images_path
- from kivymd.uix.fitimage import FitImage
- from kivymd.uix.screen import MDScreen
-
- self.render(
- MDScreen(
- FitImage(
- source=os.path.join(
- images_path, "logo", "kivymd-icon-512.png"
- ),
- size_hint=(0.5, 0.5),
- pos_hint={"center_x": 0.5, "center_y": 0.5},
- radius=[36, 36, 0, 0],
- mipmap=True,
- )
- )
- )

diff --git a/sbapp/kivymd/tests/test_font_definitions.py b/sbapp/kivymd/tests/test_font_definitions.py
deleted file mode 100644
index adf8e752..00000000
--- a/sbapp/kivymd/tests/test_font_definitions.py
+++ /dev/null
@@ -1,16 +0,0 @@
-def test_fonts_registration():
- # This should register fonts:
- from kivy.core.text import LabelBase
-
- import kivymd # NOQA
-
- fonts = [
- "Roboto",
- "RobotoThin",
- "RobotoLight",
- "RobotoMedium",
- "RobotoBlack",
- "Icons",
- ]
- for font in fonts:
- assert font in LabelBase._fonts.keys()

diff --git a/sbapp/kivymd/tests/test_icon_definitions.py b/sbapp/kivymd/tests/test_icon_definitions.py
deleted file mode 100644
index d1f6baba..00000000
--- a/sbapp/kivymd/tests/test_icon_definitions.py
+++ /dev/null
@@ -1,10 +0,0 @@
-def test_icons_have_size():
- from kivy.core.text import Label
-
- from kivymd.icon_definitions import md_icons
-
- lbl = Label(font_name="Icons")
- for icon_name, icon_value in md_icons.items():
- assert len(icon_value) == 1
- lbl.refresh()
- assert lbl.get_extents(icon_value) is not None

diff --git a/sbapp/kivymd/tests/test_imagelist.py b/sbapp/kivymd/tests/test_imagelist.py
deleted file mode 100644
index 4ca989e7..00000000
--- a/sbapp/kivymd/tests/test_imagelist.py
+++ /dev/null
@@ -1,39 +0,0 @@
-from kivymd.tests.base_test import BaseTest
-
-
-class ImageListTest(BaseTest):
- def test_imagelist_raw_app(self):
- import os
-
- from kivymd import images_path
- from kivymd.uix.button import MDIconButton
- from kivymd.uix.imagelist import MDSmartTile
- from kivymd.uix.label import MDLabel
- from kivymd.uix.screen import MDScreen
-
- self.render(
- MDScreen(
- MDSmartTile(
- MDIconButton(
- icon="heart-outline",
- theme_icon_color="Custom",
- icon_color="red",
- pos_hint={"center_y": 0.5},
- ),
- MDLabel(
- text="Julia and Julie",
- bold=True,
- color="white",
- ),
- radius=24,
- box_radius=[0, 0, 24, 24],
- box_color="grey",
- source=os.path.join(
- images_path, "logo", "kivymd-icon-512.png"
- ),
- pos_hint={"center_x": 0.5, "center_y": 0.5},
- size_hint=(None, None),
- size=("320dp", "320dp"),
- )
- )
- )

diff --git a/sbapp/kivymd/tests/test_list.py b/sbapp/kivymd/tests/test_list.py
deleted file mode 100644
index da5a0456..00000000
--- a/sbapp/kivymd/tests/test_list.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from kivymd.tests.base_test import BaseTest
-
-
-class ListTest(BaseTest):
- def test_list_raw_app(self):
- import os
-
- from kivymd import images_path
- from kivymd.uix.list import (
- IconLeftWidget,
- IconRightWidget,
- ImageLeftWidget,
- IRightBodyTouch,
- MDList,
- OneLineAvatarIconListItem,
- OneLineAvatarListItem,
- OneLineIconListItem,
- OneLineListItem,
- ThreeLineListItem,
- TwoLineListItem,
- )
- from kivymd.uix.screen import MDScreen
- from kivymd.uix.scrollview import MDScrollView
- from kivymd.uix.selectioncontrol import MDCheckbox
-
- class RightCheckbox(IRightBodyTouch, MDCheckbox):
- pass
-
- self.render(
- MDScreen(
- MDScrollView(
- MDList(
- OneLineListItem(text="Text"),
- TwoLineListItem(
- text="Text", secondary_text="secondary text"
- ),
- ThreeLineListItem(
- text="Text",
- secondary_text="secondary text",
- tertiary_text="tertiary text",
- ),
- OneLineAvatarListItem(
- ImageLeftWidget(
- source=os.path.join(
- images_path, "logo", "kivymd-icon-512.png"
- )
- ),
- text="Text",
- ),
- OneLineIconListItem(
- IconLeftWidget(icon="plus"),
- text="Text",
- ),
- OneLineAvatarIconListItem(
- IconLeftWidget(icon="plus"),
- IconRightWidget(icon="minus"),
- text="Text",
- ),
- OneLineAvatarIconListItem(
- IconLeftWidget(icon="plus"),
- RightCheckbox(),
- text="Text",
- ),
- )
- )
- )
- )

diff --git a/sbapp/kivymd/tests/test_navigationdrawer.py b/sbapp/kivymd/tests/test_navigationdrawer.py
deleted file mode 100644
index 87039c2a..00000000
--- a/sbapp/kivymd/tests/test_navigationdrawer.py
+++ /dev/null
@@ -1,94 +0,0 @@
-from kivymd.tests.base_test import BaseTest
-
-
-class NavigationDrawerTest(BaseTest):
- def test_navigationdrawer_raw_app(self):
- from kivymd.uix.navigationdrawer import (
- MDNavigationDrawer,
- MDNavigationDrawerDivider,
- MDNavigationDrawerHeader,
- MDNavigationDrawerItem,
- MDNavigationDrawerLabel,
- MDNavigationDrawerMenu,
- MDNavigationLayout,
- )
- from kivymd.uix.screen import MDScreen
- from kivymd.uix.screenmanager import MDScreenManager
- from kivymd.uix.toolbar import MDTopAppBar
-
- class DrawerClickableItem(MDNavigationDrawerItem):
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.focus_color = "#e7e4c0"
- self.unfocus_color = "#f7f4e7"
- self.text_color = "#4a4939"
- self.icon_color = "#4a4939"
- self.ripple_color = "#c5bdd2"
- self.selected_color = "#0c6c4d"
-
- class DrawerLabelItem(MDNavigationDrawerItem):
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.bg_color = "#f7f4e7"
- self.text_color = "#4a4939"
- self.icon_color = "#4a4939"
- _no_ripple_effect = True # NOQA
-
- self.app.theme_cls.material_style = "M3"
- self.render(
- MDNavigationLayout(
- MDScreenManager(
- MDScreen(
- MDTopAppBar(
- title="Navigation Drawer",
- elevation=10,
- pos_hint={"top": 1},
- md_bg_color="#e7e4c0",
- specific_text_color="#4a4939",
- left_action_items=[
- ["menu", lambda x: self.nav_drawer_open()]
- ],
- )
- )
- ),
- MDNavigationDrawer(
- MDNavigationDrawerMenu(
- MDNavigationDrawerHeader(
- title="Header title",
- title_color="#4a4939",
- text="Header text",
- spacing="4dp",
- padding=("12dp", 0, 0, "56dp"),
- ),
- MDNavigationDrawerLabel(
- text="Mail",
- ),
- DrawerClickableItem(
- icon="gmail",
- right_text="+99",
- text_right_color="#4a4939",
- text="Inbox",
- radius=24,
- ),
- DrawerClickableItem(
- icon="send",
- text="Outbox",
- radius=24,
- ),
- MDNavigationDrawerDivider(),
- MDNavigationDrawerLabel(
- text="Labels",
- ),
- DrawerLabelItem(
- icon="information-outline",
- text="Label",
- ),
- DrawerLabelItem(
- icon="information-outline",
- text="Label",
- ),
- ),
- id="nav_drawer",
- ),
- )
- )

diff --git a/sbapp/kivymd/tests/test_tab.py b/sbapp/kivymd/tests/test_tab.py
deleted file mode 100644
index 1ad59206..00000000
--- a/sbapp/kivymd/tests/test_tab.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from kivymd.tests.base_test import BaseTest
-
-
-class TabTest(BaseTest):
- def test_tab_raw_app(self):
- from kivymd.uix.floatlayout import MDFloatLayout
- from kivymd.uix.tab import MDTabs, MDTabsBase
-
- class Tab(MDFloatLayout, MDTabsBase):
- pass
-
- tab = MDTabs()
- tab.add_widget(Tab(title="Tab"))
- self.render(tab)

diff --git a/sbapp/kivymd/tests/test_textfield.py b/sbapp/kivymd/tests/test_textfield.py
deleted file mode 100644
index b5654e12..00000000
--- a/sbapp/kivymd/tests/test_textfield.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# from kivy.clock import Clock
-# from kivy.uix.textinput import TextInput
-
-from kivymd.tests.base_test import BaseTest
-
-
-class TextFieldTest(BaseTest):
- def test_textfield_raw_app(self):
- from kivymd.uix.boxlayout import MDBoxLayout
- from kivymd.uix.button import MDFlatButton
- from kivymd.uix.screen import MDScreen
- from kivymd.uix.textfield import MDTextField
-
- # def set_text():
- # for widget in self.screen.ids.box.children:
- # if issubclass(widget.__class__, TextInput):
- # widget.text = "Input text"
-
- self.render(
- MDScreen(
- MDBoxLayout(
- MDTextField(
- hint_text="Label",
- helper_text="Error massage",
- mode="rectangle",
- max_text_length=5,
- ),
- MDTextField(
- icon_left="git",
- hint_text="Label",
- helper_text="Error massage",
- mode="rectangle",
- ),
- MDTextField(
- icon_left="git",
- hint_text="Label",
- helper_text="Error massage",
- mode="fill",
- ),
- MDTextField(
- hint_text="Label",
- helper_text="Error massage",
- mode="fill",
- ),
- MDTextField(
- hint_text="Label",
- helper_text="Error massage",
- ),
- MDTextField(
- icon_left="git",
- hint_text="Label",
- helper_text="Error massage",
- ),
- MDTextField(
- hint_text="Round mode",
- mode="round",
- max_text_length=15,
- helper_text="Massage",
- ),
- MDFlatButton(
- text="SET TEXT",
- pos_hint={"center_x": 0.5},
- ),
- id="box",
- orientation="vertical",
- spacing="20dp",
- adaptive_height=True,
- size_hint_x=0.8,
- pos_hint={"center_x": 0.5, "center_y": 0.5},
- )
- )
- )

diff --git a/sbapp/kivymd/theming.py b/sbapp/kivymd/theming.py
index 7bc41168..0c89bcbc 100755
--- a/sbapp/kivymd/theming.py
+++ b/sbapp/kivymd/theming.py
@@ -606,13 +606,16 @@ class ThemeManager(EventDispatcher):
readonly.
"""
- material_style = OptionProperty("M2", options=["M2", "M3"])
+ material_style = OptionProperty("M3", options=["M2", "M3"])
"""
Material design style.
Available options are: 'M2', 'M3'.
.. versionadded:: 1.0.0
+ .. versionchanged:: 1.2.0
+ By default now `'M3'`.
+
.. seealso::
`Material Design 2 <https://material.io/>`_ and
@@ -620,7 +623,7 @@ class ThemeManager(EventDispatcher):
:attr:`material_style` is an :class:`~kivy.properties.OptionProperty`
- and defaults to `'M2'`.
+ and defaults to `'M3'`.
"""
theme_style_switch_animation = BooleanProperty(False)
@@ -647,9 +650,8 @@ class ThemeManager(EventDispatcher):
padding: 0, 0, 0 , "36dp"
size_hint: .5, .5
pos_hint: {"center_x": .5, "center_y": .5}
- elevation: 4
- shadow_radius: 6
- shadow_offset: 0, 2
+ elevation: 2
+ shadow_offset: 0, -2
MDLabel:
text: "Theme style - {}".format(app.theme_cls.theme_style)
@@ -720,9 +722,8 @@ class ThemeManager(EventDispatcher):
padding=(0, 0, 0, "36dp"),
size_hint=(0.5, 0.5),
pos_hint={"center_x": 0.5, "center_y": 0.5},
- elevation=4,
- shadow_radius=6,
- shadow_offset=(0, 2),
+ elevation=2,
+ shadow_offset=(0, -2),
)
)
)
@@ -1665,25 +1666,60 @@ class ThemableBehavior(EventDispatcher):
"https://github.com/kivymd/KivyMD/wiki/Modules-Material-App#exceptions"
)
self.theme_cls = App.get_running_app().theme_cls
+
super().__init__(**kwargs)
- # def dec_disabled(self, *args, **kwargs) -> None:
- # callabacks = self.theme_cls.get_property_observers("theme_style")
-
- # for callaback in callabacks:
- # try:
- # if hasattr(callaback, "proxy") and hasattr(
- # callaback.proxy, "theme_cls"
- # ):
- # for property_name in self.unbind_properties:
- # self.theme_cls.unbind(
- # **{
- # property_name: getattr(
- # callaback.proxy, callaback.method_name
- # )
- # }
- # )
- # except ReferenceError:
- # pass
-
- # super().dec_disabled(*args, **kwargs)
+ # Fix circular imports.
+ from kivymd.uix.behaviors import CommonElevationBehavior
+ from kivymd.uix.label import MDLabel
+ from kivymd.uix.textfield import MDTextField
+
+ self.common_elevation_behavior = CommonElevationBehavior
+ self.md_label = MDLabel
+ self.md_textfield = MDTextField
+
+ def remove_widget(self, widget) -> None:
+ if not hasattr(widget, "theme_cls"):
+ super().remove_widget(widget)
+ return
+
+ callbacks = widget.theme_cls.get_property_observers("theme_style")
+
+ for callback in callbacks:
+ try:
+ if hasattr(callback, "proxy") and hasattr(
+ callback.proxy, "theme_cls"
+ ):
+ if issubclass(widget.__class__, self.md_textfield):
+ widget.theme_cls.unbind(
+ **{
+ "theme_style": getattr(
+ callback.proxy, callback.method_name
+ )
+ }
+ )
+ for property_name in self.unbind_properties:
+ if widget == callback.proxy:
+ widget.theme_cls.unbind(
+ **{
+ property_name: getattr(
+ callback.proxy, callback.method_name
+ )
+ }
+ )
+ # KivyMD widgets may contain other MD widgets.
+ for children in widget.children:
+ if hasattr(children, "theme_cls"):
+ self.remove_widget(children)
+ except ReferenceError:
+ pass
+
+ # Canceling a scheduled method call on_window_touch for MDLabel
+ # objects.
+ if (
+ issubclass(widget.__class__, self.md_label)
+ and self.md_label.allow_selection
+ ):
+ Window.unbind(on_touch_down=widget.on_window_touch)
+
+ super().remove_widget(widget)

diff --git a/sbapp/kivymd/toast/kivytoast/kivytoast.py b/sbapp/kivymd/toast/kivytoast/kivytoast.py
index 87645bca..c1906662 100755
--- a/sbapp/kivymd/toast/kivytoast/kivytoast.py
+++ b/sbapp/kivymd/toast/kivytoast/kivytoast.py
@@ -82,7 +82,7 @@ class Toast(BaseDialog):
def __init__(self, **kwargs):
super().__init__(**kwargs)
- self.label_toast = Label(size_hint=(None, None), opacity=0)
+ self.label_toast = Label(size_hint=(None, None), markup=True, opacity=0)
self.label_toast.bind(texture_size=self.label_check_texture_size)
self.add_widget(self.label_toast)

diff --git a/sbapp/kivymd/tools/packaging/pyinstaller/hook-kivymd.py b/sbapp/kivymd/tools/packaging/pyinstaller/hook-kivymd.py
index 040d103a..d2cb9a5c 100644
--- a/sbapp/kivymd/tools/packaging/pyinstaller/hook-kivymd.py
+++ b/sbapp/kivymd/tools/packaging/pyinstaller/hook-kivymd.py
@@ -13,18 +13,6 @@ from pathlib import Path
import kivymd
datas = [
- # Add `.frag` files from the `kivymd/data/glsl/elevation` directory.
- (
- str(Path(kivymd.glsl_path).joinpath("elevation")) + os.sep,
- str(
- Path("kivymd").joinpath(
- str(Path(kivymd.glsl_path)).split(str(Path("kivymd")) + os.sep)[
- 1
- ]
- + f"{os.sep}elevation"
- )
- ),
- ),
# Add `.ttf` files from the `kivymd/fonts` directory.
(
kivymd.fonts_path,

diff --git a/sbapp/kivymd/tools/patterns/create_project.py b/sbapp/kivymd/tools/patterns/create_project.py
index 64487b02..864ed55f 100644
--- a/sbapp/kivymd/tools/patterns/create_project.py
+++ b/sbapp/kivymd/tools/patterns/create_project.py
@@ -381,13 +381,12 @@ class {name_screen}Controller:
temp_base_screen = '''from kivy.properties import ObjectProperty
from kivymd.app import MDApp
-from kivymd.theming import ThemableBehavior
from kivymd.uix.screen import MDScreen
from Utility.observer import Observer
-class BaseScreenView(ThemableBehavior, MDScreen, Observer):
+class BaseScreenView(MDScreen, Observer):
"""
A base class that implements a visual representation of the model data.
The view class must be inherited from this class.

diff --git a/sbapp/kivymd/uix/__init__.py b/sbapp/kivymd/uix/__init__.py
index 2d48cc1b..9aec2dc1 100755
--- a/sbapp/kivymd/uix/__init__.py
+++ b/sbapp/kivymd/uix/__init__.py
@@ -59,6 +59,8 @@ class MDAdaptiveWidget(SpecificBackgroundColorBehavior):
else:
if not isinstance(self, (FloatLayout, Screen)):
self.bind(minimum_height=self.setter("height"))
+ if not self.children:
+ self.height = 0
def on_adaptive_width(self, md_widget, value: bool) -> None:
self.size_hint_x = None
@@ -71,6 +73,8 @@ class MDAdaptiveWidget(SpecificBackgroundColorBehavior):
else:
if not isinstance(self, (FloatLayout, Screen)):
self.bind(minimum_width=self.setter("width"))
+ if not self.children:
+ self.width = 0
def on_adaptive_size(self, md_widget, value: bool) -> None:
self.size_hint = (None, None)
@@ -84,3 +88,5 @@ class MDAdaptiveWidget(SpecificBackgroundColorBehavior):
else:
if not isinstance(self, (FloatLayout, Screen)):
self.bind(minimum_size=self.setter("size"))
+ if not self.children:
+ self.size = (0, 0)

diff --git a/sbapp/kivymd/uix/anchorlayout.py b/sbapp/kivymd/uix/anchorlayout.py
index 4e2be4a1..fe435423 100644
--- a/sbapp/kivymd/uix/anchorlayout.py
+++ b/sbapp/kivymd/uix/anchorlayout.py
@@ -33,11 +33,14 @@ __all__ = ("MDAnchorLayout",)
from kivy.uix.anchorlayout import AnchorLayout
+from kivymd.theming import ThemableBehavior
from kivymd.uix import MDAdaptiveWidget
from kivymd.uix.behaviors import DeclarativeBehavior
-class MDAnchorLayout(DeclarativeBehavior, AnchorLayout, MDAdaptiveWidget):
+class MDAnchorLayout(
+ DeclarativeBehavior, ThemableBehavior, AnchorLayout, MDAdaptiveWidget
+):
"""
Anchor layout class. For more information, see in the
:class:`~kivy.uix.anchorlayout.AnchorLayout` class documentation.

diff --git a/sbapp/kivymd/uix/backdrop/backdrop.py b/sbapp/kivymd/uix/backdrop/backdrop.py
index 604ab13b..276eb1c7 100644
--- a/sbapp/kivymd/uix/backdrop/backdrop.py
+++ b/sbapp/kivymd/uix/backdrop/backdrop.py
@@ -201,7 +201,6 @@ from kivy.properties import (
from kivy.uix.boxlayout import BoxLayout
from kivymd import uix_path
-from kivymd.theming import ThemableBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.card import MDCard
from kivymd.uix.floatlayout import MDFloatLayout
@@ -214,8 +213,11 @@ with open(
Builder.load_string(kv_file.read())
-class MDBackdrop(MDFloatLayout, ThemableBehavior):
+class MDBackdrop(MDFloatLayout):
"""
+ For more information, see in the
+ :class:`~kivymd.uix.floatlayout.MDFloatLayout` class documentation.
+
:Events:
:attr:`on_open`
When the front layer drops.
@@ -277,7 +279,7 @@ class MDBackdrop(MDFloatLayout, ThemableBehavior):
back_layer_color = ColorProperty(None)
"""
- Background color of back layer.
+ Background color of back layer in (r, g, b, a) or string format.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop-back-layer-color.png
:align: center
@@ -288,7 +290,7 @@ class MDBackdrop(MDFloatLayout, ThemableBehavior):
front_layer_color = ColorProperty(None)
"""
- Background color of front layer.
+ Background color of front layer in (r, g, b, a) or string format.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop-front-layer-color.png
:align: center
@@ -512,15 +514,30 @@ class MDBackdrop(MDFloatLayout, ThemableBehavior):
class MDBackdropToolbar(MDTopAppBar):
- """Implements a toolbar for back content."""
+ """
+ Implements a toolbar for back content.
+
+ For more information, see in the
+ :class:`~kivymd.uix.toolbar.toolbar.MDTopAppBar` classes documentation.
+ """
class MDBackdropFrontLayer(MDBoxLayout):
- """Container for front content."""
+ """
+ Container for front content.
+
+ For more information, see in the
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` classes documentation.
+ """
class MDBackdropBackLayer(MDBoxLayout):
- """Container for back content."""
+ """
+ Container for back content.
+
+ For more information, see in the
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation.
+ """
class _BackLayer(BoxLayout):

diff --git a/sbapp/kivymd/uix/banner/banner.py b/sbapp/kivymd/uix/banner/banner.py
index fb87221d..534783c9 100644
--- a/sbapp/kivymd/uix/banner/banner.py
+++ b/sbapp/kivymd/uix/banner/banner.py
@@ -177,6 +177,13 @@ with open(
class MDBanner(MDCard):
+ """
+ Banner class.
+
+ For more information, see in the :class:`~kivymd.uix.card.MDCard`
+ class documentation.
+ """
+
vertical_pad = NumericProperty(dp(68))
"""
Indent the banner at the top of the screen.

diff --git a/sbapp/kivymd/uix/behaviors/__init__.py b/sbapp/kivymd/uix/behaviors/__init__.py
index e89ed10a..d80d8c18 100755
--- a/sbapp/kivymd/uix/behaviors/__init__.py
+++ b/sbapp/kivymd/uix/behaviors/__init__.py
@@ -20,6 +20,11 @@ from .elevation import (
RectangularElevationBehavior,
RoundedRectangularElevationBehavior,
)
+from .motion_behavior import (
+ MotionDialogBehavior,
+ MotionShackBehavior,
+ MotionDropDownMenuBehavior,
+)
from .magic_behavior import MagicBehavior
from .ripple_behavior import CircularRippleBehavior, RectangularRippleBehavior
from .rotate_behavior import RotateBehavior

diff --git a/sbapp/kivymd/uix/behaviors/backgroundcolor_behavior.py b/sbapp/kivymd/uix/behaviors/backgroundcolor_behavior.py
index 4c4649bf..a24166d6 100755
--- a/sbapp/kivymd/uix/behaviors/backgroundcolor_behavior.py
+++ b/sbapp/kivymd/uix/behaviors/backgroundcolor_behavior.py
@@ -5,9 +5,9 @@ Behaviors/Background Color
.. note:: The following classes are intended for in-house use of the library.
"""
-__all__ = ("BackgroundColorBehavior", "SpecificBackgroundColorBehavior")
+from __future__ import annotations
-from typing import List, Union
+__all__ = ("BackgroundColorBehavior", "SpecificBackgroundColorBehavior")
from kivy.animation import Animation
from kivy.lang import Builder
@@ -49,6 +49,8 @@ Builder.load_string(
source: root.background
Color:
rgba: self.line_color if self.line_color else (0, 0, 0, 0)
+ # TODO: maybe we should use SmoothLine,
+ # but this should be tested on all widgets.
Line:
width: root.line_width
rounded_rectangle:
@@ -58,7 +60,6 @@ Builder.load_string(
self.width, \
self.height, \
*self.radius, \
- 100, \
]
PopMatrix
""",
@@ -90,6 +91,8 @@ class BackgroundColorBehavior:
and defaults to `[0, 0, 0, 0]`.
"""
+ # FIXME: in this case, we will not be able to animate this property
+ # using the `Animation` class.
md_bg_color = ColorProperty([1, 1, 1, 0])
"""
The background color of the widget (:class:`~kivy.uix.widget.Widget`)
@@ -154,12 +157,34 @@ class BackgroundColorBehavior:
_background_y = NumericProperty(0)
_background_origin = ReferenceListProperty(_background_x, _background_y)
_md_bg_color = ColorProperty([0, 0, 0, 0])
+ _origin_line_color = ColorProperty(None)
+ _origin_md_bg_color = ColorProperty(None)
def __init__(self, **kwarg):
super().__init__(**kwarg)
- self.bind(pos=self.update_background_origin)
+ self.bind(
+ pos=self.update_background_origin,
+ disabled=self.restore_color_origin,
+ )
+
+ def restore_color_origin(self, instance_md_widget, value: bool) -> None:
+ """Called when the values of :attr:`disabled` change."""
+
+ if not value:
+ if self._origin_line_color:
+ self.line_color = self._origin_line_color
+ if self._origin_md_bg_color:
+ self.md_bg_color = self._origin_md_bg_color
+
+ def on_line_color(self, instance_md_widget, value: list | str) -> None:
+ """Called when the values of :attr:`line_color` change."""
+
+ if not self.disabled:
+ self._origin_line_color = value
+
+ def on_md_bg_color(self, instance_md_widget, color: list | str):
+ """Called when the values of :attr:`md_bg_color` change."""
- def on_md_bg_color(self, instance_md_widget, color: Union[list, str]):
if (
hasattr(self, "theme_cls")
and self.theme_cls.theme_style_switch_animation
@@ -172,9 +197,12 @@ class BackgroundColorBehavior:
else:
self._md_bg_color = color
- def update_background_origin(
- self, instance_md_widget, pos: List[float]
- ) -> None:
+ if not self.disabled:
+ self._origin_md_bg_color = color
+
+ def update_background_origin(self, instance_md_widget, pos: list) -> None:
+ """Called when the values of :attr:`pos` change."""
+
if self.background_origin:
self._background_origin = self.background_origin
else:

diff --git a/sbapp/kivymd/uix/behaviors/elevation.py b/sbapp/kivymd/uix/behaviors/elevation.py
index 615edacf..f081d5c1 100755
--- a/sbapp/kivymd/uix/behaviors/elevation.py
+++ b/sbapp/kivymd/uix/behaviors/elevation.py
@@ -41,8 +41,9 @@ For example, let's create a button with a rectangular elevation effect:
# With elevation effect
RectangularElevationButton:
pos_hint: {"center_x": .5, "center_y": .6}
- elevation: 4.5
- shadow_offset: 0, 6
+ elevation: 4
+ shadow_offset: 0, -6
+ shadow_softness: 4
# Without elevation effect
RectangularElevationButton:
@@ -102,8 +103,9 @@ For example, let's create a button with a rectangular elevation effect:
MDScreen(
RectangularElevationButton(
pos_hint={"center_x": .5, "center_y": .6},
- elevation=4.5,
- shadow_offset=(0, 6),
+ elevation=4,
+ shadow_softness=4,
+ shadow_offset=(0, -6),
),
RectangularElevationButton(
pos_hint={"center_x": .5, "center_y": .4},
@@ -164,6 +166,7 @@ Similarly, create a circular button:
CircularElevationButton:
pos_hint: {"center_x": .5, "center_y": .6}
elevation: 4
+ shadow_softness: 4
'''
@@ -231,6 +234,7 @@ Similarly, create a circular button:
CircularElevationButton(
pos_hint={"center_x": .5, "center_y": .5},
elevation=4,
+ shadow_softness=4,
)
)
)
@@ -266,7 +270,7 @@ Animating the elevation
size_hint: None, None
size: 100, 100
md_bg_color: 0, 0, 1, 1
- elevation: 4
+ elevation: 2
radius: 18
'''
@@ -336,7 +340,7 @@ Animating the elevation
size_hint=(None, None),
size=(100, 100),
md_bg_color="blue",
- elevation=4,
+ elevation=2,
radius=18,
)
)
@@ -360,32 +364,62 @@ __all__ = (
"FakeCircularElevationBehavior",
)
-import os
-
from kivy import Logger
-from kivy.clock import Clock
-from kivy.core.window import Window
-from kivy.graphics import RenderContext, RoundedRectangle
+from kivy.lang import Builder
from kivy.properties import (
- AliasProperty,
- BooleanProperty,
BoundedNumericProperty,
ColorProperty,
ListProperty,
NumericProperty,
- ObjectProperty,
VariableListProperty,
)
from kivy.uix.widget import Widget
-from kivymd import glsl_path
-from kivymd.app import MDApp
+Builder.load_string(
+ """
+<CommonElevationBehavior>
+ canvas.before:
+ PushMatrix
+ Scale:
+ x: self.scale_value_x
+ y: self.scale_value_y
+ z: self.scale_value_x
+ origin:
+ self.center \
+ if not self.scale_value_center else \
+ self.scale_value_center
+ Rotate:
+ angle: self.rotate_value_angle
+ axis: tuple(self.rotate_value_axis)
+ origin: self.center
+ Color:
+ rgba:
+ (0, 0, 0, 0) \
+ if self.disabled or not self.elevation else \
+ root.shadow_color
+ BoxShadow:
+ pos: self.pos
+ size: self.size
+ offset: root.shadow_offset
+ spread_radius: -(root.shadow_softness), -(root.shadow_softness)
+ blur_radius: root.elevation * 10
+ border_radius:
+ (root.radius if hasattr(self, "radius") else [0, 0, 0, 0]) \
+ if root.shadow_radius == [0.0, 0.0, 0.0, 0.0] else \
+ root.shadow_radius
+ canvas.after:
+ PopMatrix
+"""
+)
-# FIXME: Add shadow manipulation with canvas instructions such as
-# PushMatrix and PopMatrix.
class CommonElevationBehavior(Widget):
- """Common base class for rectangular and circular elevation behavior."""
+ """
+ Common base class for rectangular and circular elevation behavior.
+
+ For more information, see in the :class:`~kivy.uix.widget.Widget`
+ class documentation.
+ """
elevation = BoundedNumericProperty(0, min=0, errorvalue=0)
"""
@@ -418,9 +452,9 @@ class CommonElevationBehavior(Widget):
radius: 12, 46, 12, 46
size_hint: .5, .3
pos_hint: {"center_x": .5, "center_y": .5}
- elevation: 4
- shadow_softness: 8
- shadow_offset: (-2, 2)
+ elevation: 2
+ shadow_softness: 4
+ shadow_offset: (2, -2)
'''
@@ -434,21 +468,11 @@ class CommonElevationBehavior(Widget):
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-radius.png
:align: center
- .. note::
- However, if you want to use this parameter, remember that the angle
- values for the radius of the Kivy widgets and the radius for the shader
- are different.
-
- .. code-block:: python
-
- shadow_radius = ['top-right', 'bot-right', 'top-left', 'bot-left']
- kivy_radius = ['top-left', 'top-right', 'bottom-right', 'bottom-left']
-
:attr:`shadow_radius` is an :class:`~kivy.properties.VariableListProperty`
and defaults to `[0, 0, 0, 0]`.
"""
- shadow_softness = NumericProperty(12)
+ shadow_softness = NumericProperty(0.0)
"""
Softness of the shadow.
@@ -482,7 +506,9 @@ class CommonElevationBehavior(Widget):
class RectangularElevationButton(CommonElevationBehavior, BackgroundColorBehavior):
- md_bg_color = [0, 0, 1, 1]
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.md_bg_color = "blue"
class Example(MDApp):
@@ -499,7 +525,19 @@ class CommonElevationBehavior(Widget):
and defaults to `12`.
"""
- shadow_offset = ListProperty((0, 2))
+ shadow_softness_size = BoundedNumericProperty(2, min=2, deprecated=True)
+ """
+ The value of the softness of the shadow.
+
+ .. versionadded:: 1.1.0
+
+ .. deprecated:: 1.2.0
+
+ :attr:`shadow_softness_size` is an :class:`~kivy.properties.NumericProperty`
+ and defaults to `2`.
+ """
+
+ shadow_offset = ListProperty((0, 0))
"""
Offset of the shadow.
@@ -523,14 +561,16 @@ class CommonElevationBehavior(Widget):
RectangularElevationButton:
pos_hint: {"center_x": .5, "center_y": .5}
elevation: 6
- shadow_radius: 18
- shadow_softness: 24
- shadow_offset: 12, 12
+ shadow_radius: 6
+ shadow_softness: 12
+ shadow_offset: -12, -12
'''
class RectangularElevationButton(CommonElevationBehavior, BackgroundColorBehavior):
- md_bg_color = [0, 0, 1, 1]
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.md_bg_color = "blue"
class Example(MDApp):
@@ -546,7 +586,7 @@ class CommonElevationBehavior(Widget):
.. code-block:: kv
RectangularElevationButton:
- shadow_offset: -12, 12
+ shadow_offset: 12, -12
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-offset-2.png
:align: center
@@ -554,7 +594,7 @@ class CommonElevationBehavior(Widget):
.. code-block:: kv
RectangularElevationButton:
- shadow_offset: -12, -12
+ shadow_offset: 12, 12
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-offset-3.png
:align: center
@@ -562,13 +602,13 @@ class CommonElevationBehavior(Widget):
.. code-block:: kv
RectangularElevationButton:
- shadow_offset: 12, -12
+ shadow_offset: -12, 12
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-offset-4.png
:align: center
:attr:`shadow_offset` is an :class:`~kivy.properties.ListProperty`
- and defaults to `(0, 2)`.
+ and defaults to `(0, 0)`.
"""
shadow_color = ColorProperty([0, 0, 0, 0.6])
@@ -586,252 +626,75 @@ class CommonElevationBehavior(Widget):
:align: center
:attr:`shadow_color` is an :class:`~kivy.properties.ColorProperty`
- and defaults to `[0.4, 0.4, 0.4, 0.8]`.
+ and defaults to `[0, 0, 0, 0.6]`.
"""
- _transition_ref = ObjectProperty()
- _has_relative_position = BooleanProperty(defaultvalue=False)
- _elevation = 0
- _shadow_color = [0.0, 0.0, 0.0, 0.0]
+ scale_value_x = NumericProperty(1)
+ """
+ X-axis value.
- def _get_window_pos(self, *args):
- window_pos = self.to_window(*self.pos)
- # To list, so it can be compared to self.pos directly.
- return [window_pos[0], window_pos[1]]
+ .. versionadded:: 1.2.0
- def _set_window_pos(self, value):
- self.window_pos = value
+ :attr:`scale_value_x` is an :class:`~kivy.properties.NumericProperty`
+ and defaults to `1`.
+ """
- window_pos = AliasProperty(_get_window_pos, _set_window_pos)
+ scale_value_y = NumericProperty(1)
+ """
+ Y-axis value.
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
+ .. versionadded:: 1.2.0
- if hasattr(MDApp.get_running_app(), "shaders_disabled") and MDApp.get_running_app().shaders_disabled:
- self.shaders_disabled = True
- else:
- self.shaders_disabled = False
-
- with self.canvas.before:
- self.context = RenderContext(use_parent_projection=True)
- with self.context:
- if self.shaders_disabled:
- self.rect = None
- del self.rect
- else:
- self.rect = RoundedRectangle(pos=self.pos, size=self.size)
-
- self.after_init()
-
- def after_init(self, *args):
- Clock.schedule_once(self.check_for_relative_behavior)
- if not self.shaders_disabled:
- Clock.schedule_once(self.set_shader_string)
- Clock.schedule_once(lambda x: self.on_elevation(self, self.elevation))
- self.on_pos()
-
- def check_for_relative_behavior(self, *args) -> None:
- """
- Checks if the widget has relative properties and if necessary
- binds Window.on_draw and screen events to fix behavior
- """
-
- if self.pos != self.window_pos:
- self._has_relative_position = True
-
- # Loops to check if its inside screenmanager or bottom_navigation.
- widget = self
- while True:
- # Checks if has screen event function
- # works for Screen and MDTab objects.
- if hasattr(widget, "on_pre_enter"):
- widget.bind(on_pre_enter=self.apply_correction)
- widget.bind(on_pre_leave=self.apply_correction)
- widget.bind(on_enter=self.reset_correction)
- widget.bind(on_leave=self.reset_correction)
- self._has_relative_position = True
-
- # Save refs to objects with transition property.
- if hasattr(widget, "header"): # specific to bottom_nav
- self._transition_ref = widget.header.panel
- elif hasattr(widget, "manager"): # specific to screen
- if widget.manager: # manager cant be None
- self._transition_ref = widget.manager
- break
-
- elif widget.parent and str(widget) != str(widget.parent):
- widget = widget.parent
- else:
- break
-
- if self._has_relative_position:
- Window.bind(on_draw=self.update_window_position)
-
- def apply_correction(self, *args):
- if self._transition_ref:
- transition = str(self._transition_ref.transition)
- # Slide and Card transitions only need _has_relative_pos to be
- # always on.
- if (
- "SlideTransition" in transition
- or "CardTransition" in transition
- ):
- self.context.use_parent_modelview = False
- else:
- self.context.use_parent_modelview = True
-
- def reset_correction(self, *args):
- self.context.use_parent_modelview = False
- self.update_window_position()
-
- def get_shader_string(self) -> str:
- shader_string = ""
- for name_file in ["header.frag", "elevation.frag", "main.frag"]:
- with open(
- os.path.join(glsl_path, "elevation", name_file),
- encoding="utf-8",
- ) as file:
- shader_string += f"{file.read()}\n\n"
-
- return shader_string
-
- def set_shader_string(self, *args) -> None:
- self.context["shadow_radius"] = list(map(float, self.shadow_radius))
- self.context["shadow_softness"] = float(self.shadow_softness)
- self.context["shadow_color"] = list(map(float, self.shadow_color))[
- :-1
- ] + [float(self.opacity)]
- self.context["pos"] = list(map(float, self.rect.pos))
- self.context.shader.fs = self.get_shader_string()
-
- def update_resolution(self) -> None:
- self.context["resolution"] = (*self.rect.size, *self.rect.pos)
-
- def on_shadow_color(self, instance, value) -> None:
- def on_shadow_color(*args):
- self._shadow_color = list(map(float, value))[:-1] + [
- float(self.opacity) if not self.disabled else 0
- ]
- self.context["shadow_color"] = self._shadow_color
-
- Clock.schedule_once(on_shadow_color)
-
- def on_shadow_radius(self, instance, value) -> None:
- def on_shadow_radius(*args):
- if hasattr(self, "context"):
- self.context["shadow_radius"] = list(map(float, value))
-
- Clock.schedule_once(on_shadow_radius)
-
- def on_shadow_softness(self, instance, value) -> None:
- def on_shadow_softness(*args):
- if hasattr(self, "context"):
- self.context["shadow_softness"] = float(value)
-
- Clock.schedule_once(on_shadow_softness)
+ :attr:`scale_value_y` is an :class:`~kivy.properties.NumericProperty`
+ and defaults to `1`.
+ """
- def on_elevation(self, instance, value) -> None:
- def on_elevation(*args):
- if hasattr(self, "context"):
- self._elevation = value
- self.hide_elevation(
- True if (value <= 0 or self.disabled) else False
- )
-
- Clock.schedule_once(on_elevation)
-
- def on_shadow_offset(self, instance, value) -> None:
- self.on_size()
- self.on_pos()
-
- def update_window_position(self, *args) -> None:
- """
- This function is used only when the widget has relative position
- properties.
- """
-
- self.on_pos()
-
- def on_pos(self, *args) -> None:
- if not hasattr(self, "rect"):
- return
-
- if (
- self._has_relative_position
- and not self.context.use_parent_modelview
- ):
- pos = self.window_pos
- else:
- pos = self.pos
-
- self.rect.pos = [
- pos[0]
- - ((self.rect.size[0] - self.width) / 2)
- - self.shadow_offset[0],
- pos[1]
- - ((self.rect.size[1] - self.height) / 2)
- - self.shadow_offset[1],
- ]
-
- self.context["mouse"] = [self.rect.pos[0], 0.0, 0.0, 0.0]
- self.context["pos"] = list(map(float, self.rect.pos))
- self.update_resolution()
-
- def on_size(self, *args) -> None:
- if not hasattr(self, "rect"):
- return
-
- # If the elevation value is 0, set the canvas size to zero.
- # Because even with a zero elevation value, the shadow is displayed
- # under the widget. This is visible if we change the scale
- # of the widget.
- width = self.size[0] if self.elevation else 0
- height = self.size[1] if self.elevation else 0
- self.rect.size = (
- width + (self._elevation * self.shadow_softness / 2),
- height + (self._elevation * self.shadow_softness / 2),
- )
+ scale_value_z = NumericProperty(1)
+ """
+ Z-axis value.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`scale_value_z` is an :class:`~kivy.properties.NumericProperty`
+ and defaults to `1`.
+ """
+
+ scale_value_center = ListProperty()
+ """
+ Origin of the scale.
+
+ .. versionadded:: 1.2.0
+
+ The format of the origin can be either (x, y) or (x, y, z).
+
+ :attr:`scale_value_center` is an :class:`~kivy.properties.NumericProperty`
+ and defaults to `[]`.
+ """
+
+ rotate_value_angle = NumericProperty(0)
+ """
+ Property for getting/setting the angle of the rotation.
+
+ .. versionadded:: 1.2.0
- self.context["mouse"] = [self.rect.pos[0], 0.0, 0.0, 0.0]
- self.context["size"] = list(map(float, self.rect.size))
- self.update_resolution()
-
- def on_opacity(self, instance, value: int | float) -> None:
- """
- Adjusts the transparency of the shadow according to the transparency
- of the widget.
- """
-
- def on_opacity(*args):
- self._shadow_color = list(map(float, self._shadow_color))[:-1] + [
- float(value)
- ]
- self.context["shadow_color"] = self._shadow_color
-
- super().on_opacity(instance, value)
- Clock.schedule_once(on_opacity)
-
- def on_radius(self, instance, value) -> None:
- self.shadow_radius = [value[1], value[2], value[0], value[3]]
-
- def on_disabled(self, instance, value) -> None:
- if value:
- self._elevation = 0
- self.hide_elevation(True)
- else:
- self.hide_elevation(False)
-
- def hide_elevation(self, hide: bool) -> None:
- if hide:
- self._elevation = -self.elevation
- self._shadow_color = [0.0, 0.0, 0.0, 0.0]
- else:
- self._elevation = self.elevation
- self._shadow_color = self.shadow_color[:-1] + [float(self.opacity)]
-
- self.on_shadow_color(self, self._shadow_color)
- self.on_size()
- self.on_pos()
+ :attr:`rotate_value_angle` is an :class:`~kivy.properties.NumericProperty`
+ and defaults to `0`.
+ """
+
+ rotate_value_axis = ListProperty((0, 0, 1))
+ """
+ Property for getting/setting the axis of the rotation.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`rotate_value_axis` is an :class:`~kivy.properties.ListProperty`
+ and defaults to `(0, 0, 1)`.
+ """
+
+ _elevation = 0
+
+ def on_elevation(self, instance, value) -> None:
+ self._elevation = value
class RectangularElevationBehavior(CommonElevationBehavior):

diff --git a/sbapp/kivymd/uix/behaviors/focus_behavior.py b/sbapp/kivymd/uix/behaviors/focus_behavior.py
index b90f16a6..33e00bdd 100644
--- a/sbapp/kivymd/uix/behaviors/focus_behavior.py
+++ b/sbapp/kivymd/uix/behaviors/focus_behavior.py
@@ -15,8 +15,9 @@ Usage
from kivy.lang import Builder
from kivymd.app import MDApp
- from kivymd.uix.behaviors import RectangularElevationBehavior, FocusBehavior
+ from kivymd.uix.behaviors import RectangularElevationBehavior
from kivymd.uix.boxlayout import MDBoxLayout
+ from kivymd.uix.behaviors.focus_behavior import FocusBehavior
KV = '''
MDScreen:
@@ -72,6 +73,18 @@ from kivymd.uix.behaviors import HoverBehavior
class FocusBehavior(HoverBehavior, ButtonBehavior):
+ """
+ Focus behavior class.
+
+ For more information, see in the :class:`~kivymd.uix.behavior.HoverBehavior`
+ and :class:`~kivy.uix.button.ButtonBehavior` classes documentation.
+
+ :Events:
+ :attr:`on_enter`
+ Called when mouse enters the bbox of the widget AND the widget is visible
+ :attr:`on_leave`
+ Called when the mouse exits the widget AND the widget is visible
+ """
focus_behavior = BooleanProperty(True)
"""

diff --git a/sbapp/kivymd/uix/behaviors/hover_behavior.py b/sbapp/kivymd/uix/behaviors/hover_behavior.py
index 40f519e9..dbe1ce2b 100644
--- a/sbapp/kivymd/uix/behaviors/hover_behavior.py
+++ b/sbapp/kivymd/uix/behaviors/hover_behavior.py
@@ -11,13 +11,13 @@ In `KV file`:
.. code-block:: kv
- <HoverItem@MDBoxLayout+ThemableBehavior+HoverBehavior>
+ <HoverItem@MDBoxLayout+HoverBehavior>
In `python file`:
.. code-block:: python
- class HoverItem(MDBoxLayout, ThemableBehavior, HoverBehavior):
+ class HoverItem(MDBoxLayout, HoverBehavior):
'''Custom item implementing hover behavior.'''
After creating a class, you must define two methods for it:
@@ -38,7 +38,6 @@ the widget.
from kivymd.app import MDApp
from kivymd.uix.behaviors import HoverBehavior
from kivymd.uix.boxlayout import MDBoxLayout
- from kivymd.theming import ThemableBehavior
KV = '''
Screen
@@ -51,7 +50,7 @@ the widget.
'''
- class HoverItem(MDBoxLayout, ThemableBehavior, HoverBehavior):
+ class HoverItem(MDBoxLayout, HoverBehavior):
'''Custom item implementing hover behavior.'''
def on_enter(self, *args):

diff --git a/sbapp/kivymd/uix/behaviors/magic_behavior.py b/sbapp/kivymd/uix/behaviors/magic_behavior.py
index 9e7f862b..8f7e9983 100644
--- a/sbapp/kivymd/uix/behaviors/magic_behavior.py
+++ b/sbapp/kivymd/uix/behaviors/magic_behavior.py
@@ -118,7 +118,6 @@ Builder.load_string(
class MagicBehavior:
-
magic_speed = NumericProperty(1)
"""
Animation playback speed.

diff --git a/sbapp/kivymd/uix/behaviors/motion_behavior.py b/sbapp/kivymd/uix/behaviors/motion_behavior.py
new file mode 100644
index 00000000..45a8e4b2
--- /dev/null
+++ b/sbapp/kivymd/uix/behaviors/motion_behavior.py
@@ -0,0 +1,287 @@
+"""
+Behaviors/Motion
+================
+
+.. rubric:: Use motion to make a UI expressive and easy to use.
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/motion.png
+ :align: center
+
+.. versionadded:: 1.2.0
+
+Classes of the `Motion` type implement the display behavior of widgets such
+as dialogs, dropdown menu, snack bars, and so on.
+"""
+
+__all__ = (
+ "MotionBase",
+ "MotionDropDownMenuBehavior",
+ "MotionDialogBehavior",
+ "MotionShackBehavior",
+)
+
+from kivy.animation import Animation
+from kivy.clock import Clock
+from kivy.core.window import Window
+from kivy.properties import StringProperty, NumericProperty
+
+from kivymd.uix.behaviors.stencil_behavior import StencilBehavior
+
+
+class MotionBase:
+ """Base class for widget display movement behavior."""
+
+ show_transition = StringProperty("linear")
+ """
+ The type of transition of the widget opening.
+
+ :attr:`show_transition` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `'linear'`.
+ """
+
+ show_duration = NumericProperty(0.2)
+ """
+ Duration of widget display transition.
+
+ :attr:`show_duration` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `0.2`.
+ """
+
+ hide_transition = StringProperty("linear")
+ """
+ The type of transition of the widget closing.
+
+ :attr:`hide_transition` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `'linear'`.
+ """
+
+ hide_duration = NumericProperty(0.2)
+ """
+ Duration of widget closing transition.
+
+ :attr:`hide_duration` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `0.2`.
+ """
+
+
+class MotionDropDownMenuBehavior(MotionBase):
+ """
+ Base class for the dropdown menu movement behavior.
+
+ For more information, see in the :class:`~MotionBase` class documentation.
+ """
+
+ show_transition = StringProperty("out_back")
+ """
+ The type of transition of the widget opening.
+
+ :attr:`show_transition` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `'out_back'`.
+ """
+
+ show_duration = NumericProperty(0.4)
+ """
+ Duration of widget display transition.
+
+ :attr:`show_duration` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `0.2`.
+ """
+
+ hide_transition = StringProperty("out_cubic")
+ """
+ The type of transition of the widget closing.
+
+ :attr:`hide_transition` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `'out_cubic'`.
+ """
+
+ _scale_x = NumericProperty(None)
+ """
+ Default X-axis scaling values.
+
+ :attr:`_scale_x` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `None`.
+ """
+
+ _scale_y = NumericProperty(None)
+ """
+ Default Y-axis scaling values.
+
+ :attr:`_scale_y` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `None`.
+ """
+
+ _opacity = NumericProperty(None)
+ """
+ Menu transparency values.
+
+ :attr:`_opacity` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `None`.
+ """
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.set_scale()
+ # self.set_opacity()
+
+ def set_opacity(self) -> None:
+ self._opacity = 0
+
+ def set_scale(self) -> None:
+ self._scale_x = 0
+ self._scale_y = 0
+
+ def on_dismiss(self) -> None:
+ Window.remove_widget(self)
+ # anim = Animation(
+ # _scale_x=0,
+ # _scale_y=0,
+ # # _opacity=0,
+ # duration=self.hide_duration,
+ # transition=self.hide_transition,
+ # )
+ # anim.bind(on_complete=lambda *args: Window.remove_widget(self))
+ # anim.start(self)
+
+ def on_open(self, *args):
+ pass
+ anim = Animation(
+ _scale_y=1,
+ # _opacity=1,
+ duration=0.0,
+ transition=self.show_transition,
+ )
+ anim &= Animation(
+ _scale_x=1,
+ duration=0.0,
+ transition="out_quad",
+ )
+ anim.start(self)
+
+ def on__opacity(self, instance, value):
+ self.opacity = value
+
+ def on__scale_x(self, instance, value):
+ self.scale_value_x = value
+
+ def on__scale_y(self, instance, value):
+ self.scale_value_y = value
+
+
+class MotionDialogBehavior(MotionBase):
+ """
+ Base class for dialog movement behavior.
+
+ For more information, see in the :class:`~MotionBase` class documentation.
+ """
+
+ show_duration = NumericProperty(0.0)
+ """
+ Duration of widget display transition.
+
+ :attr:`show_duration` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `0.1`.
+ """
+
+ scale_x = NumericProperty(1.0)
+ """
+ Default X-axis scaling values.
+
+ :attr:`scale_x` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `1.5`.
+ """
+
+ scale_y = NumericProperty(1.0)
+ """
+ Default Y-axis scaling values.
+
+ :attr:`scale_y` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `1.5`.
+ """
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.set_default_values()
+
+ def set_default_values(self):
+ """Sets default scaled and transparency values."""
+
+ self.scale_value_x = self.scale_x
+ self.scale_value_y = self.scale_y
+ self.opacity = 0
+
+ def on_dismiss(self, *args):
+ """Called when a dialog closed."""
+
+ self.set_default_values()
+
+ def on_open(self, *args):
+ """Called when a dialog opened."""
+
+ Animation(
+ opacity=1,
+ scale_value_x=1,
+ scale_value_y=1,
+ t=self.show_transition,
+ d=self.show_duration,
+ ).start(self)
+
+
+class MotionShackBehavior(StencilBehavior, MotionBase):
+ """
+ The base class for the behavior of the movement of snack bars.
+
+ For more information, see in the
+ :class:`~MotionBase` class and
+ :class:`~kivy.uix.behaviors.stencil_behavior.StencilBehavior` class
+ documentation.
+ """
+
+ _interval = 0
+ _height = 0
+
+ def on_dismiss(self, *args):
+ """Called when a snackbar closed."""
+
+ def remove_snackbar(*args):
+ Window.parent.remove_widget(self)
+ self.height = self._height
+ self.dispatch("on_dismiss")
+
+ Clock.unschedule(self._wait_interval)
+ anim = Animation(
+ opacity=0,
+ height=0,
+ t=self.hide_transition,
+ d=self.hide_duration,
+ )
+ anim.bind(on_complete=remove_snackbar)
+ anim.start(self)
+
+ def on_open(self, *args):
+ """Called when a snackbar opened."""
+
+ def open(*args):
+ self._height = self.height
+ self.height = 0
+ anim = Animation(
+ opacity=1,
+ height=self._height,
+ t=self.show_transition,
+ d=self.show_duration,
+ )
+ anim.bind(
+ on_complete=lambda *args: Clock.schedule_interval(
+ self._wait_interval, 1
+ )
+ )
+ anim.start(self)
+
+ Clock.schedule_once(open)
+ self.dispatch("on_open")
+
+ def _wait_interval(self, interval):
+ self._interval += interval
+ if self._interval > self.duration:
+ self.dismiss()
+ self._interval = 0

diff --git a/sbapp/kivymd/uix/behaviors/ripple_behavior.py b/sbapp/kivymd/uix/behaviors/ripple_behavior.py
old mode 100755
new mode 100644
index 12718781..fddcef70
--- a/sbapp/kivymd/uix/behaviors/ripple_behavior.py
+++ b/sbapp/kivymd/uix/behaviors/ripple_behavior.py
@@ -413,7 +413,12 @@ class CommonRipple:
class RectangularRippleBehavior(CommonRipple):
- """Class implements a rectangular ripple effect."""
+ """
+ Class implements a rectangular ripple effect.
+
+ For more information, see in the :class:`~kivymd.uix.behavior.CommonRipple`
+ class documentation.
+ """
ripple_scale = NumericProperty(2.75)
"""
@@ -472,7 +477,12 @@ class RectangularRippleBehavior(CommonRipple):
class CircularRippleBehavior(CommonRipple):
- """Class implements a circular ripple effect."""
+ """
+ Class implements a circular ripple effect.
+
+ For more information, see in the :class:`~kivymd.uix.behavior.CommonRipple`
+ class documentation.
+ """
ripple_scale = NumericProperty(1)
"""

diff --git a/sbapp/kivymd/uix/behaviors/rotate_behavior.py b/sbapp/kivymd/uix/behaviors/rotate_behavior.py
index c80b8793..c3f6fdfe 100644
--- a/sbapp/kivymd/uix/behaviors/rotate_behavior.py
+++ b/sbapp/kivymd/uix/behaviors/rotate_behavior.py
@@ -91,6 +91,10 @@ KivyMD
Test().run()
+
+.. warning:: Do not use `RotateBehavior` class with classes that inherited`
+ from `CommonElevationBehavior` class. `CommonElevationBehavior` classes
+ by default contains attributes for rotate widget.
"""
__all__ = ("RotateBehavior",)

diff --git a/sbapp/kivymd/uix/behaviors/scale_behavior.py b/sbapp/kivymd/uix/behaviors/scale_behavior.py
index bfc1e036..85fb7b4f 100644
--- a/sbapp/kivymd/uix/behaviors/scale_behavior.py
+++ b/sbapp/kivymd/uix/behaviors/scale_behavior.py
@@ -105,12 +105,16 @@ KivyMD
Test().run()
+
+.. warning:: Do not use `ScaleBehavior` class with classes that inherited`
+ from `CommonElevationBehavior` class. `CommonElevationBehavior` classes
+ by default contains attributes for scale widget.
"""
__all__ = ("ScaleBehavior",)
from kivy.lang import Builder
-from kivy.properties import NumericProperty
+from kivy.properties import ListProperty, NumericProperty
Builder.load_string(
"""
@@ -120,8 +124,11 @@ Builder.load_string(
Scale:
x: self.scale_value_x
y: self.scale_value_y
- z: self.scale_value_x
- origin: self.center
+ z: self.scale_value_z
+ origin:
+ self.center \
+ if not self.scale_value_center else \
+ self.scale_value_center
canvas.after:
PopMatrix
"""
@@ -154,3 +161,15 @@ class ScaleBehavior:
:attr:`scale_value_z` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
+
+ scale_value_center = ListProperty()
+ """
+ Origin of the scale.
+
+ .. versionadded:: 1.2.0
+
+ The format of the origin can be either (x, y) or (x, y, z).
+
+ :attr:`scale_value_center` is an :class:`~kivy.properties.NumericProperty`
+ and defaults to `[]`.
+ """

diff --git a/sbapp/kivymd/uix/behaviors/touch_behavior.py b/sbapp/kivymd/uix/behaviors/touch_behavior.py
index 49b54fcf..8aca7bf2 100644
--- a/sbapp/kivymd/uix/behaviors/touch_behavior.py
+++ b/sbapp/kivymd/uix/behaviors/touch_behavior.py
@@ -22,7 +22,7 @@ Usage
from kivymd.uix.button import MDRaisedButton
KV = '''
- Screen:
+ MDScreen:
MyButton:
text: "PRESS ME"
@@ -74,9 +74,10 @@ class TouchBehavior:
def create_clock(self, widget, touch, *args):
if self.collide_point(touch.x, touch.y):
- callback = partial(self.on_long_touch, touch)
- Clock.schedule_once(callback, self.duration_long_touch)
- touch.ud["event"] = callback
+ if "event" not in touch.ud:
+ callback = partial(self.on_long_touch, touch)
+ Clock.schedule_once(callback, self.duration_long_touch)
+ touch.ud["event"] = callback
if touch.is_double_tap:
self.on_double_tap(touch, *args)
@@ -85,10 +86,9 @@ class TouchBehavior:
def delete_clock(self, widget, touch, *args):
if self.collide_point(touch.x, touch.y):
- try:
+ if "event" in touch.ud:
Clock.unschedule(touch.ud["event"])
- except KeyError:
- pass
+ del touch.ud["event"]
def on_long_touch(self, touch, *args):
"""Called when the widget is pressed for a long time."""

diff --git a/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.py b/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.py
old mode 100755
new mode 100644
index d99f54b0..9b764d4e
--- a/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.py
+++ b/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.py
@@ -274,12 +274,19 @@ with open(
Builder.load_string(kv_file.read())
-class MDBottomNavigationHeader(
- ThemableBehavior, ButtonBehavior, MDAnchorLayout
-):
+class MDBottomNavigationHeader(ButtonBehavior, MDAnchorLayout):
+ """
+ Bottom navigation header class.
+
+ For more information, see in the
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~kivymd.uix.anchorlayout.MDAnchorLayout`
+ classes documentation.
+ """
+
panel_color = ColorProperty([1, 1, 1, 0])
"""
- Panel color of bottom navigation.
+ Panel color of bottom navigation in (r, g, b, a) or string format.
:attr:`panel_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[1, 1, 1, 0]`.
@@ -307,7 +314,8 @@ class MDBottomNavigationHeader(
text_color_normal = ColorProperty([1, 1, 1, 1])
"""
- Text color of the label when it is not selected.
+ Text color in (r, g, b, a) or string format of the label when it is not
+ selected.
:attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[1, 1, 1, 1]`.
@@ -315,7 +323,7 @@ class MDBottomNavigationHeader(
text_color_active = ColorProperty([1, 1, 1, 1])
"""
- Text color of the label when it is selected.
+ Text color in (r, g, b, a) or string format of the label when it is selected.
:attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[1, 1, 1, 1]`.
@@ -323,7 +331,8 @@ class MDBottomNavigationHeader(
selected_color_background = ColorProperty(None)
"""
- The background color of the highlighted item when using Material Design v3.
+ The background color in (r, g, b, a) or string format of the highlighted
+ item when using Material Design v3.
.. versionadded:: 1.0.0
@@ -384,10 +393,13 @@ class MDBottomNavigationHeader(
)
-class MDTab(MDScreen, ThemableBehavior):
+class MDTab(MDScreen):
"""
A tab is simply a screen with meta information that defines the content
that goes in the tab header.
+
+ For more information, see in the
+ :class:`~kivymd.uix.screen.MDScreen` class documentation.
"""
__events__ = (
@@ -524,6 +536,10 @@ class TabbedPanelBase(
A class that contains all variables a :class:`~kivy.properties.TabPannel`
must have. It is here so I (zingballyhoo) don't get mad about
the :class:`~kivy.properties.TabbedPannels` not being DRY.
+
+ For more information, see in the :class:`~kivymd.theming.ThemableBehavior`
+ and :class:`~kivymd.uix.behaviors.SpecificBackgroundColorBehavior`
+ and :class:`~kivy.uix.boxlayout.BoxLayout` classes documentation.
"""
current = StringProperty(None)
@@ -555,6 +571,10 @@ class MDBottomNavigation(DeclarativeBehavior, TabbedPanelBase):
A bottom navigation that is implemented by delegating all items to a
:class:`~kivy.uix.screenmanager.ScreenManager`.
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and
+ :class:`~TabbedPanelBase` classes documentation.
+
:Events:
:attr:`on_switch_tabs`
Called when switching tabs. Returns the object of the tab to be
@@ -856,7 +876,5 @@ class MDBottomNavigation(DeclarativeBehavior, TabbedPanelBase):
return bottom_navigation_item
-class MDBottomNavigationBar(
- ThemableBehavior, CommonElevationBehavior, MDFloatLayout
-):
+class MDBottomNavigationBar(CommonElevationBehavior, MDFloatLayout):
pass

diff --git a/sbapp/kivymd/uix/bottomsheet/__init__.py b/sbapp/kivymd/uix/bottomsheet/__init__.py
index 439811d7..dec1c230 100644
--- a/sbapp/kivymd/uix/bottomsheet/__init__.py
+++ b/sbapp/kivymd/uix/bottomsheet/__init__.py
@@ -1,7 +1,10 @@
# NOQA F401
from .bottomsheet import (
- GridBottomSheetItem,
MDBottomSheet,
+ MDBottomSheetContent,
+ MDBottomSheetDragHandle,
+ MDBottomSheetDragHandleButton,
+ MDBottomSheetDragHandleTitle,
MDCustomBottomSheet,
MDGridBottomSheet,
MDListBottomSheet,

diff --git a/sbapp/kivymd/uix/bottomsheet/bottomsheet.kv b/sbapp/kivymd/uix/bottomsheet/bottomsheet.kv
index a8406296..6d63e1c2 100644
--- a/sbapp/kivymd/uix/bottomsheet/bottomsheet.kv
+++ b/sbapp/kivymd/uix/bottomsheet/bottomsheet.kv
@@ -1,73 +1,42 @@
-#:import Window kivy.core.window.Window
-
-
-<SheetList>
-
- MDGridLayout:
- id: box_sheet_list
- cols: 1
- adaptive_height: True
- padding: 0, 0, 0, "96dp"
-
-
-<MDBottomSheet>
- md_bg_color: root.value_transparent
- _upper_padding: _upper_padding
- _gl_content: _gl_content
- _position_content: Window.height
-
- MDBoxLayout:
- orientation: "vertical"
- padding: 0, 1, 0, 0
-
- BsPadding:
- id: _upper_padding
- size_hint_y: None
- height: root.height - min(root.width * 9 / 16, root._gl_content.height)
- on_release: root.dismiss()
+<MDBottomSheetContent>
+ size_hint_y: None
+ height: self.minimum_height
- BottomSheetContent:
- id: _gl_content
- size_hint_y: None
- cols: 1
- md_bg_color: 0, 0, 0, 0
- canvas:
- Color:
- rgba: root.theme_cls.bg_normal if not root.bg_color else root.bg_color
- RoundedRectangle:
- pos: self.pos
- size: self.size
- radius:
- [
- (root.radius, root.radius) if root.radius_from == "top_left" or root.radius_from == "top" else (0, 0),
- (root.radius, root.radius) if root.radius_from == "top_right" or root.radius_from == "top" else (0, 0),
- (root.radius, root.radius) if root.radius_from == "bottom_right" or root.radius_from == "bottom" else (0, 0),
- (root.radius, root.radius) if root.radius_from == "bottom_left" or root.radius_from == "bottom" else (0, 0)
- ]
+<MDBottomSheetDragHandle>
+ orientation: "vertical"
+ size_hint_y: None
+ height: self.minimum_height
+ padding: "16dp", "8dp", "16dp", "16dp"
+ BottomSheetDragHandle:
+ md_bg_color:
+ app.theme_cls.disabled_hint_text_color \
+ if not root.drag_handle_color else \
+ root.drag_handle_color
+ size_hint: None, None
+ size: "32dp", "4dp"
+ radius: 4
+ pos_hint: {"center_x": .5}
-<ListBottomSheetIconLeft>
- theme_text_color: "Primary"
- pos_hint: {"center_x": .5, "center_y": .5}
+ BottomSheetDragHandleContainer:
+ id: header_container
+ size_hint_y: None
+ height: self.minimum_height
-<GridBottomSheetItem>
+<MDBottomSheet>
orientation: "vertical"
- padding: 0, dp(24), 0, 0
- size_hint_y: None
- size: dp(64), dp(96)
-
- AnchorLayout:
- anchor_x: "center"
+ md_bg_color: root.bg_color if root.bg_color else app.theme_cls.bg_darkest
+ radius: 16, 16, 0, 0
+ padding: 0, "8dp", 0, 0
- MDIconButton:
- icon: root.source
- user_font_size: root.icon_size
- on_release: root.dispatch("on_release")
+ MDBoxLayout:
+ id: drag_handle_container
+ size_hint_y: None
+ height: self.minimum_height
- MDLabel:
- font_style: "Caption"
- theme_text_color: "Secondary"
- text: root.caption
- halign: "center"
+ MDBoxLayout:
+ id: container
+ size_hint_y: None
+ height: self.minimum_height
\ No newline at end of file

diff --git a/sbapp/kivymd/uix/bottomsheet/bottomsheet.py b/sbapp/kivymd/uix/bottomsheet/bottomsheet.py
old mode 100755
new mode 100644
index 522f31ba..4961ff02
--- a/sbapp/kivymd/uix/bottomsheet/bottomsheet.py
+++ b/sbapp/kivymd/uix/bottomsheet/bottomsheet.py
@@ -4,232 +4,564 @@ Components/BottomSheet
.. seealso::
- `Material Design spec, Sheets: bottom <https://material.io/components/sheets-bottom>`_
+ `Material Design spec, Sheets: bottom <https://m3.material.io/components/bottom-sheets/overview>`_
.. rubric:: Bottom sheets are surfaces containing supplementary content that are anchored to the bottom of the screen.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet.png
:align: center
-Two classes are available to you :class:`~MDListBottomSheet` and :class:`~MDGridBottomSheet`
-for standard bottom sheets dialogs:
+Usage
+=====
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/grid-list-bottomsheets.png
+.. code-block:: kv
+
+ MDScreen:
+
+ [ Content screen ]
+
+ MDBottomSheet:
+
+The bottom sheet has two types:
+
+- Standard_
+- Modal_
+
+.. Standard:
+Standard
+--------
+
+`Standard bottom sheets <https://m3.material.io/components/bottom-sheets/guidelines#aa1caae4-2d86-4c8c-af09-548a6f666b8a>`_
+co-exist with the screen’s main UI region and allow for simultaneously viewing
+and interacting with both regions, especially when the main UI region is
+frequently scrolled or panned.
+
+Use a standard bottom sheet to display content that complements the screen’s
+primary content, such as an audio player in a music app.
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-standard.png
:align: center
-Usage :class:`~MDListBottomSheet`
-=================================
+Standard bottom sheets are elevated above the main UI region so their
+visibility is not affected by panning or scrolling.
+
+Standard bottom sheet example
+-----------------------------
+
+.. tabs::
+
+ .. tab:: Declarative KV style
+
+ .. code-block:: python
+
+ from kivy.lang import Builder
+
+ from kivymd.app import MDApp
+
+ KV = '''
+ MDScreen:
+
+ MDBoxLayout:
+ orientation: "vertical"
+ padding: "12dp"
+ adaptive_height: True
+ pos_hint: {"top": 1}
+
+ MDSmartTile:
+ id: smart_tile
+ source: "https://picsum.photos/id/70/3011/2000"
+ radius: 16
+ box_radius: [0, 0, 16, 16]
+ size_hint_y: None
+ height: "240dp"
+ on_release:
+ bottom_sheet.open() \\
+ if bottom_sheet.state == "close" else \\
+ bottom_sheet.dismiss()
+
+ MDLabel:
+ bold: True
+ color: 1, 1, 1, 1
+ text:
+ "Tap to open the bottom sheet" \\
+ if bottom_sheet.state == "close" else \\
+ "Tap to close the bottom sheet"
+
+ MDBottomSheet:
+ id: bottom_sheet
+ type: "standard"
+ bg_color: "grey"
+ default_opening_height: smart_tile.y - dp(12)
+ size_hint_y: None
+ height: root.height - (smart_tile.height + dp(24))
+ '''
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ return Builder.load_string(KV)
+
+
+ Example().run()
+
+ .. tab:: Declarative python style
+
+ .. code-block:: python
+
+ from kivy.clock import Clock
+ from kivy.metrics import dp
+
+ from kivymd.app import MDApp
+ from kivymd.uix.bottomsheet import MDBottomSheet
+ from kivymd.uix.boxlayout import MDBoxLayout
+ from kivymd.uix.imagelist import MDSmartTile
+ from kivymd.uix.label import MDLabel
+ from kivymd.uix.screen import MDScreen
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ return MDScreen(
+ MDBoxLayout(
+ MDSmartTile(
+ MDLabel(
+ id="tile_label",
+ text="Tap to open the bottom sheet",
+ bold=True,
+ color=(1, 1, 1, 1),
+ ),
+ id="smart_tile",
+ source="https://picsum.photos/id/70/3011/2000",
+ radius=16,
+ box_radius=[0, 0, 16, 16],
+ size_hint_y=None,
+ height="240dp",
+ ),
+ id="box",
+ orientation="vertical",
+ padding="12dp",
+ pos_hint={"top": 1},
+ adaptive_height=True,
+ ),
+ MDBottomSheet(
+ id="bottom_sheet",
+ size_hint_y=None,
+ type="standard",
+ bg_color="grey",
+ ),
+ )
+
+ def open_bottom_sheet(self, *args):
+ bottom_sheet = self.root.ids.bottom_sheet
+ smart_tile = self.root.ids.box.ids.smart_tile
+ tile_label = smart_tile.ids.tile_label
+ bottom_sheet.open() if bottom_sheet.state == "close" else bottom_sheet.dismiss()
+ tile_label.text = (
+ "Tap to open the bottom sheet"
+ if bottom_sheet.state == "close"
+ else "Tap to close the bottom sheet"
+ )
+
+ def on_start(self):
+ def on_start(*args):
+ bottom_sheet = self.root.ids.bottom_sheet
+ smart_tile = self.root.ids.box.ids.smart_tile
+ bottom_sheet.default_opening_height = smart_tile.y - dp(12)
+ bottom_sheet.height = self.root.height - (
+ smart_tile.height + dp(24)
+ )
+ smart_tile.bind(on_release=lambda x: self.open_bottom_sheet())
+
+ Clock.schedule_once(on_start, 1.2)
+
+
+ Example().run()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-standard-example.gif
+ :align: center
-.. code-block:: python
+.. Modal:
+Modal
+-----
- from kivy.lang import Builder
+Like dialogs, `modal bottom sheets <https://m3.material.io/components/bottom-sheets/guidelines#1cb775b6-6d2b-4d50-96ad-1862727e986b>`_
+appear in front of app content, disabling all other app functionality when
+they appear, and remaining on screen until confirmed, dismissed, or a required
+action has been taken.
- from kivymd.toast import toast
- from kivymd.uix.bottomsheet import MDListBottomSheet
- from kivymd.app import MDApp
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-modal.png
+ :align: center
- KV = '''
- MDScreen:
+Modal bottom sheet example
+--------------------------
+
+.. tabs::
+
+ .. tab:: Declarative KV style
+
+ .. code-block:: python
+
+ from kivy.lang import Builder
+
+ from kivymd.app import MDApp
+
+ KV = '''
+ MDScreen:
+
+ MDBoxLayout:
+ orientation: "vertical"
+ padding: "12dp"
+ adaptive_height: True
+ pos_hint: {"top": 1}
+
+ MDSmartTile:
+ id: smart_tile
+ source: "https://picsum.photos/id/70/3011/2000"
+ radius: 16
+ box_radius: [0, 0, 16, 16]
+ size_hint_y: None
+ height: "240dp"
+ on_release: bottom_sheet.open()
+
+ MDLabel:
+ bold: True
+ color: 1, 1, 1, 1
+ text: "Tap to open the modal bottom sheet"
+
+ MDBottomSheet:
+ id: bottom_sheet
+ bg_color: "grey"
+ default_opening_height: smart_tile.y - dp(12)
+ size_hint_y: None
+ height: root.height - (smart_tile.height + dp(24))
+ '''
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ return Builder.load_string(KV)
+
+
+ Example().run()
+
+ .. tab:: Declarative python style
+
+ .. code-block:: python
+
+ from kivy.clock import Clock
+ from kivy.metrics import dp
+
+ from kivymd.app import MDApp
+ from kivymd.uix.bottomsheet import MDBottomSheet
+ from kivymd.uix.boxlayout import MDBoxLayout
+ from kivymd.uix.imagelist import MDSmartTile
+ from kivymd.uix.label import MDLabel
+ from kivymd.uix.screen import MDScreen
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ return MDScreen(
+ MDBoxLayout(
+ MDSmartTile(
+ MDLabel(
+ id="tile_label",
+ text="Tap to open the modal bottom sheet",
+ bold=True,
+ color=(1, 1, 1, 1),
+ ),
+ id="smart_tile",
+ source="https://picsum.photos/id/70/3011/2000",
+ radius=16,
+ box_radius=[0, 0, 16, 16],
+ size_hint_y=None,
+ height="240dp",
+ ),
+ id="box",
+ orientation="vertical",
+ padding="12dp",
+ pos_hint={"top": 1},
+ adaptive_height=True,
+ ),
+ MDBottomSheet(
+ id="bottom_sheet",
+ size_hint_y=None,
+ bg_color="grey",
+ ),
+ )
+
+ def open_bottom_sheet(self, *args):
+ bottom_sheet = self.root.ids.bottom_sheet
+ bottom_sheet.open()
+
+ def on_start(self):
+ def on_start(*args):
+ bottom_sheet = self.root.ids.bottom_sheet
+ smart_tile = self.root.ids.box.ids.smart_tile
+ bottom_sheet.default_opening_height = smart_tile.y - dp(12)
+ bottom_sheet.height = self.root.height - (
+ smart_tile.height + dp(24)
+ )
+ smart_tile.bind(on_release=lambda x: self.open_bottom_sheet())
+
+ Clock.schedule_once(on_start, 1.2)
+
+
+ Example().run()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-modal-example.gif
+ :align: center
- MDTopAppBar:
- title: "Example BottomSheet"
- pos_hint: {"top": 1}
- elevation: 4
+Tapping the scrim dismisses a modal bottom sheet.
- MDRaisedButton:
- text: "Open list bottom sheet"
- on_release: app.show_example_list_bottom_sheet()
- pos_hint: {"center_x": .5, "center_y": .5}
- '''
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-modal-tapping.png
+ :align: center
+Custom positioning
+------------------
- class Example(MDApp):
- def build(self):
- return Builder.load_string(KV)
+The optional drag handle provides an affordance for custom sheet height,
+or for a quick toggle through preset heights.
- def callback_for_menu_items(self, *args):
- toast(args[0])
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-drag-handle.png
+ :align: center
- def show_example_list_bottom_sheet(self):
- bottom_sheet_menu = MDListBottomSheet()
- for i in range(1, 11):
- bottom_sheet_menu.add_item(
- f"Standart Item {i}",
- lambda x, y=i: self.callback_for_menu_items(
- f"Standart Item {y}"
- ),
- )
- bottom_sheet_menu.open()
+.. code-block:: kv
+ MDBottomSheet:
- Example().run()
+ MDBottomSheetDragHandle:
-The :attr:`~MDListBottomSheet.add_item` method of the :class:`~MDListBottomSheet`
-class takes the following arguments:
+By default, when you drag and then release the drag handle, the bottom sheet
+will be closed or expand to the full screen, depending on whether you released
+the drag handle closer to the top or to the bottom of the screen:
-``text`` - element text;
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-drag-handle.gif
+ :align: center
-``callback`` - function that will be called when clicking on an item;
+In order to manually adjust the height of the bottom sheet with the drag handle,
+set the `auto_positioning` parameter to `False`:
-There is also an optional argument ``icon``,
-which will be used as an icon to the left of the item:
+.. code-block:: kv
+
+ MDBottomSheet:
+ auto_positioning: False
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/icon-list-bottomsheets.png
+ MDBottomSheetDragHandle:
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-drag-handle-auto-positioning.gif
:align: center
-.. rubric:: Using the :class:`~MDGridBottomSheet` class is similar
- to using the :class:`~MDListBottomSheet` class:
+Add elements to :class:`~MDBottomSheetDragHandleTitle` class
+------------------------------------------------------------
-.. code-block:: python
+.. code-block:: kv
- from kivy.lang import Builder
+ MDBottomSheet:
- from kivymd.toast import toast
- from kivymd.uix.bottomsheet import MDGridBottomSheet
- from kivymd.app import MDApp
+ MDBottomSheetDragHandle:
- KV = '''
- MDScreen:
+ MDBottomSheetDragHandleTitle:
+ text: "MDBottomSheet"
+ adaptive_height: True
+ font_style: "H6"
+ pos_hint: {"center_y": .5}
- MDTopAppBar:
- title: 'Example BottomSheet'
- pos_hint: {"top": 1}
- elevation: 4
+ MDBottomSheetDragHandleButton:
+ icon: "close"
- MDRaisedButton:
- text: "Open grid bottom sheet"
- on_release: app.show_example_grid_bottom_sheet()
- pos_hint: {"center_x": .5, "center_y": .5}
- '''
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-drag-handle-elements.png
+ :align: center
+Add custom content to :class:`~MDBottomSheet` class
+---------------------------------------------------
- class Example(MDApp):
- def build(self):
- return Builder.load_string(KV)
+To add custom content to the bottom sheet, use the
+:class:`~MDBottomSheetContent` class:
- def callback_for_menu_items(self, *args):
- toast(args[0])
-
- def show_example_grid_bottom_sheet(self):
- bottom_sheet_menu = MDGridBottomSheet()
- data = {
- "Facebook": "facebook-box",
- "YouTube": "youtube",
- "Twitter": "twitter-box",
- "Da Cloud": "cloud-upload",
- "Camera": "camera",
- }
- for item in data.items():
- bottom_sheet_menu.add_item(
- item[0],
- lambda x, y=item[0]: self.callback_for_menu_items(y),
- icon_src=item[1],
- )
- bottom_sheet_menu.open()
+.. code-block:: kv
+ MDBottomSheet:
+ bg_color: "darkgrey"
+ type: "standard"
+ max_opening_height: self.height
+ default_opening_height: self.max_opening_height
+ adaptive_height: True
- Example().run()
+ MDBottomSheetDragHandle:
+ drag_handle_color: "grey"
+
+ MDBottomSheetContent:
+ padding: "16dp"
+
+ MDLabel:
+ text: "Content"
+ halign: "center"
+ font_style: "H5"
+ adaptive_height: True
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/grid-bottomsheet.png
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-content.png
:align: center
-.. rubric:: You can use custom content for bottom sheet dialogs:
+A practical example with standard bottom sheet
+----------------------------------------------
+
+(A double tap on the map to open the bottom sheet)
.. code-block:: python
from kivy.lang import Builder
- from kivy.factory import Factory
+ from kivy.properties import StringProperty, ObjectProperty, BooleanProperty
+ from kivy_garden.mapview import MapView
- from kivymd.uix.bottomsheet import MDCustomBottomSheet
from kivymd.app import MDApp
+ from kivymd.uix.behaviors import TouchBehavior
+ from kivymd.uix.boxlayout import MDBoxLayout
+ from kivymd.utils import asynckivy
KV = '''
- <ItemForCustomBottomSheet@OneLineIconListItem>
- on_press: app.custom_sheet.dismiss()
- icon: ""
-
- IconLeftWidget:
- icon: root.icon
+ #:import MapSource kivy_garden.mapview.MapSource
+ #:import asynckivy kivymd.utils.asynckivy
- <ContentCustomSheet@BoxLayout>:
+ <TypeMapElement>
orientation: "vertical"
- size_hint_y: None
- height: "400dp"
+ adaptive_height: True
+ spacing: "8dp"
- MDTopAppBar:
- title: 'Custom bottom sheet:'
+ MDIconButton:
+ id: icon
+ icon: root.icon
+ md_bg_color: "#EDF1F9" if not root.selected else app.theme_cls.primary_color
+ pos_hint: {"center_x": .5}
+ theme_icon_color: "Custom"
+ icon_color: "white" if root.selected else "black"
+ on_release: app.set_active_element(root, root.title.lower())
- ScrollView:
+ MDLabel:
+ font_size: "14sp"
+ text: root.title
+ pos_hint: {"center_x": .5}
+ halign: "center"
+ adaptive_height: True
- MDGridLayout:
- cols: 1
- adaptive_height: True
- ItemForCustomBottomSheet:
- icon: "page-previous"
- text: "Preview"
+ MDScreen:
- ItemForCustomBottomSheet:
- icon: "exit-to-app"
- text: "Exit"
+ CustomMapView:
+ bottom_sheet: bottom_sheet
+ map_source: MapSource(url=app.map_sources[app.current_map])
+ lat: 46.5124
+ lon: 47.9812
+ zoom: 12
+
+ MDBottomSheet:
+ id: bottom_sheet
+ elevation: 2
+ shadow_softness: 6
+ bg_color: "white"
+ type: "standard"
+ max_opening_height: self.height
+ default_opening_height: self.max_opening_height
+ adaptive_height: True
+ on_open: asynckivy.start(app.generate_content())
+
+ MDBottomSheetDragHandle:
+ drag_handle_color: "grey"
+
+ MDBottomSheetDragHandleTitle:
+ text: "Select type map"
+ adaptive_height: True
+ bold: True
+ pos_hint: {"center_y": .5}
+
+ MDBottomSheetDragHandleButton:
+ icon: "close"
+ _no_ripple_effect: True
+ on_release: bottom_sheet.dismiss()
+
+ MDBottomSheetContent:
+ id: content_container
+ padding: 0, 0, 0, "16dp"
+ '''
- MDScreen:
+ class TypeMapElement(MDBoxLayout):
+ selected = BooleanProperty(False)
+ icon = StringProperty()
+ title = StringProperty()
- MDTopAppBar:
- title: 'Example BottomSheet'
- pos_hint: {"top": 1}
- elevation: 4
- MDRaisedButton:
- text: "Open custom bottom sheet"
- on_release: app.show_example_custom_bottom_sheet()
- pos_hint: {"center_x": .5, "center_y": .5}
- '''
+ class CustomMapView(MapView, TouchBehavior):
+ bottom_sheet = ObjectProperty()
+
+ def on_double_tap(self, touch, *args):
+ if self.bottom_sheet:
+ self.bottom_sheet.open()
class Example(MDApp):
- custom_sheet = None
+ map_sources = {
+ "street": "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}",
+ "sputnik": "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
+ "hybrid": "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}",
+ }
+ current_map = StringProperty("street")
+
+ async def generate_content(self):
+ icons = {
+ "street": "google-street-view",
+ "sputnik": "space-station",
+ "hybrid": "map-legend",
+ }
+ if not self.root.ids.content_container.children:
+ for i, title in enumerate(self.map_sources.keys()):
+ await asynckivy.sleep(0)
+ self.root.ids.content_container.add_widget(
+ TypeMapElement(
+ title=title.capitalize(),
+ icon=icons[title],
+ selected=not i,
+ )
+ )
+
+ def set_active_element(self, instance, type_map):
+ for element in self.root.ids.content_container.children:
+ if instance == element:
+ element.selected = True
+ self.current_map = type_map
+ else:
+ element.selected = False
def build(self):
return Builder.load_string(KV)
- def show_example_custom_bottom_sheet(self):
- self.custom_sheet = MDCustomBottomSheet(screen=Factory.ContentCustomSheet())
- self.custom_sheet.open()
-
Example().run()
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/custom-bottomsheet.png
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-sheet-real-example.gif
:align: center
-.. note:: When you use the :attr:`~MDCustomBottomSheet` class, you must specify
- the height of the user-defined content exactly, otherwise ``dp(100)``
- heights will be used for your ``ContentCustomSheet`` class:
-
-.. code-block:: kv
-
- <ContentCustomSheet@BoxLayout>:
- orientation: "vertical"
- size_hint_y: None
- height: "400dp"
-
-.. note:: The height of the bottom sheet dialog will never exceed half
- the height of the screen!
"""
__all__ = (
+ "MDCustomBottomSheet",
"MDGridBottomSheet",
- "GridBottomSheetItem",
"MDListBottomSheet",
- "MDCustomBottomSheet",
"MDBottomSheet",
+ "MDBottomSheetContent",
+ "MDBottomSheetDragHandle",
+ "MDBottomSheetDragHandleTitle",
+ "MDBottomSheetDragHandleButton",
)
import os
+from kivy import Logger
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.core.window import Window
@@ -243,18 +575,15 @@ from kivy.properties import (
OptionProperty,
StringProperty,
)
-from kivy.uix.behaviors import ButtonBehavior
-from kivy.uix.boxlayout import BoxLayout
-from kivy.uix.floatlayout import FloatLayout
-from kivy.uix.gridlayout import GridLayout
-from kivy.uix.modalview import ModalView
-from kivy.uix.scrollview import ScrollView
-
-from kivymd import images_path, uix_path
-from kivymd.theming import ThemableBehavior
-from kivymd.uix.behaviors import BackgroundColorBehavior
-from kivymd.uix.label import MDIcon
-from kivymd.uix.list import ILeftBody, OneLineIconListItem, OneLineListItem
+from kivy.uix.screenmanager import Screen
+
+from kivymd import uix_path
+from kivymd.uix.behaviors import CommonElevationBehavior, TouchBehavior
+from kivymd.uix.boxlayout import MDBoxLayout
+from kivymd.uix.button import MDIconButton
+from kivymd.uix.label import MDLabel
+from kivymd.uix.screen import MDScreen
+from kivymd.uix.widget import MDWidget
with open(
os.path.join(uix_path, "bottomsheet", "bottomsheet.kv"),
@@ -263,247 +592,572 @@ with open(
Builder.load_string(kv_file.read())
-class SheetList(ScrollView):
+class BottomSheetDragHandle(MDWidget):
pass
-class BsPadding(ButtonBehavior, FloatLayout):
+class BottomSheetDragHandleContainer(MDBoxLayout):
pass
-class BottomSheetContent(BackgroundColorBehavior, GridLayout):
- pass
+class BottomSheetScrimLayer(MDWidget):
+ """
+ Implements a transparency layer to shade the parent widget
+ on which the bottom sheet is displayed.
+ """
-class MDBottomSheet(ThemableBehavior, ModalView):
- background = f"{images_path}transparent.png"
- """Private attribute."""
+class MDBottomSheetContent(MDBoxLayout):
+ """
+ Implements a container for custom content for the :class:`~MDBottomSheet`
+ class
- duration_opening = NumericProperty(0.15)
+ For more information, see in the
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation.
+
+ .. versionadded:: 1.2.0
"""
- The duration of the bottom sheet dialog opening animation.
- :attr:`duration_opening` is an :class:`~kivy.properties.NumericProperty`
- and defaults to `0.15`.
+
+class MDBottomSheetDragHandleButton(MDIconButton):
"""
+ Implements a close button (or other functionality) for the
+ :class:`~MDBottomSheetDragHandle` container.
- duration_closing = NumericProperty(0.15)
+ For more information, see in the
+ :class:`~kivymd.uix.button.MDIconButton` class documentation.
+
+ .. versionadded:: 1.2.0
"""
- The duration of the bottom sheet dialog closing animation.
- :attr:`duration_closing` is an :class:`~kivy.properties.NumericProperty`
- and defaults to `0.15`.
+
+class MDBottomSheetDragHandleTitle(MDLabel):
"""
+ Implements a header for the :class:`~MDBottomSheetDragHandle` container.
- radius = NumericProperty(25)
+ For more information, see in the
+ :class:`~kivymd.uix.label.MDLabel` class documentation.
+
+ .. versionadded:: 1.2.0
"""
- The value of the rounding of the corners of the dialog.
- :attr:`radius` is an :class:`~kivy.properties.NumericProperty`
- and defaults to `25`.
+
+class MDBottomSheetDragHandle(MDBoxLayout):
"""
+ Implements a container that can place the header of the bottom sheet
+ and the close button. Also implements the event of dragging the
+ bottom sheet on the parent screen.
- radius_from = OptionProperty(
- None,
- options=[
- "top_left",
- "top_right",
- "top",
- "bottom_right",
- "bottom_left",
- "bottom",
- ],
- allownone=True,
- )
+ For more information, see in the
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation.
+
+ .. versionadded:: 1.2.0
"""
- Sets which corners to cut from the dialog. Available options are:
- (`"top_left"`, `"top_right"`, `"top"`, `"bottom_right"`, `"bottom_left"`, `"bottom"`).
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-radius-from.png
+ drag_handle_color = ColorProperty(None)
+ """
+ Color of drag handle element in (r, g, b, a) or string format.
+
+ .. code-block:: kv
+
+ MDBottomSheet:
+
+ MDBottomSheetDragHandle:
+ drag_handle_color: "white"
+
+ .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-sheet-drag-handle-color.png
:align: center
- :attr:`radius_from` is an :class:`~kivy.properties.OptionProperty`
+ :attr:`drag_handle_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
- animation = BooleanProperty(False)
+ def add_widget(self, widget, *args, **kwargs):
+ if isinstance(
+ widget,
+ (MDBottomSheetDragHandleTitle, MDBottomSheetDragHandleButton),
+ ):
+ self.ids.header_container.add_widget(widget)
+ elif isinstance(
+ widget,
+ (BottomSheetDragHandleContainer, BottomSheetDragHandle),
+ ):
+ return super().add_widget(widget)
+
+
+class MDBottomSheet(MDBoxLayout, CommonElevationBehavior, TouchBehavior):
+ """
+ Bottom sheet class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` and
+ :class:`~kivymd.uix.behaviors.touch_behavior.CommonElevationBehavior` and
+ :class:`~kivymd.uix.behaviors.touch_behavior.TouchBehavior`
+ classes documentation.
+
+ :Events:
+ `on_open`
+ Event when opening the bottom sheet.
+ `on_close`
+ Event when closing the bottom sheet.
+ `on_progress`
+ Bottom sheet opening/closing progress event.
+ """
+
+ auto_dismiss = BooleanProperty(True)
"""
- Whether to use animation for opening and closing of the bottomsheet or not.
+ This property determines if the view is automatically
+ dismissed when the user clicks outside it.
- :attr:`animation` is an :class:`~kivy.properties.BooleanProperty`
- and defaults to `False`.
+ .. versionadded:: 1.2.0
+
+ :attr:`auto_dismiss` is a :class:`~kivy.properties.BooleanProperty`
+ and defaults to `True`.
"""
- bg_color = ColorProperty(None)
+ type = OptionProperty("modal", options=["modal", "standard"])
"""
- Dialog background color in ``rgba`` format.
+ Type sheet. There are two types of bottom sheets: standard and modal.
+ Available options are: `'modal'`, `'standard'`.
- :attr:`bg_color` is an :class:`~kivy.properties.ColorProperty`
- and defaults to `[]`.
+ .. versionadded:: 1.2.0
+
+ :attr:`type` is an :class:`~kivy.properties.OptionProperty`
+ and defaults to `'modal`.
"""
- value_transparent = ColorProperty([0, 0, 0, 0.8])
+ auto_positioning = BooleanProperty(True)
"""
- Background transparency value when opening a dialog.
+ Close or expand the bottom menu automatically when you release the
+ drag handle.
- :attr:`value_transparent` is an :class:`~kivy.properties.ColorProperty`
- and defaults to `[0, 0, 0, 0.8]`.
+ .. versionadded:: 1.2.0
+
+ :attr:`auto_positioning` is an :class:`~kivy.properties.BooleanProperty`
+ and defaults to `True`.
"""
- _upper_padding = ObjectProperty()
- _gl_content = ObjectProperty()
- _position_content = NumericProperty()
+ max_opening_height = NumericProperty(None, allownone=True)
+ """
+ The maximum height a that the bottom sheet can be opened using the
+ drag handle.
- def open(self, *args):
- super().open(*args)
+ .. versionadded:: 1.2.0
- def add_widget(self, widget, index=0, canvas=None):
- super().add_widget(widget, index, canvas)
+ .. code-block:: kv
- def dismiss(self, *args, **kwargs):
- def dismiss(*args):
- self.dispatch("on_pre_dismiss")
- self._gl_content.clear_widgets()
- self._real_remove_widget()
- self.dispatch("on_dismiss")
+ MDBottomSheet:
+ max_opening_height: "300dp"
- if self.animation:
- a = Animation(height=0, d=self.duration_closing)
- a.bind(on_complete=dismiss)
- a.start(self._gl_content)
- else:
- dismiss()
+ MDBottomSheetDragHandle:
- def resize_content_layout(self, content, layout, interval=0):
- if not layout.ids.get("box_sheet_list"):
- _layout = layout
- else:
- _layout = layout.ids.box_sheet_list
+ .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-max-opening-height.gif
+ :align: center
- if _layout.height > Window.height / 2:
- height = Window.height / 2
- else:
- height = _layout.height
+ :attr:`max_opening_height` is an :class:`~kivy.properties.BooleanProperty`
+ and defaults to `None`.
+ """
- if self.animation:
- Animation(height=height, d=self.duration_opening).start(_layout)
- Animation(height=height, d=self.duration_opening).start(content)
- else:
- layout.height = height
- content.height = height
+ opening_transition = StringProperty("out_cubic")
+ """
+ The name of the animation transition type to use when animating to
+ the :attr:`state` `'open'`.
+ .. versionadded:: 1.2.0
-class ListBottomSheetIconLeft(ILeftBody, MDIcon):
- pass
+ :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `'out_cubic'`.
+ """
+ closing_transition = StringProperty("out_sine")
+ """The name of the animation transition type to use when animating to
+ the :attr:`state` 'close'.
-class MDCustomBottomSheet(MDBottomSheet):
- screen = ObjectProperty()
+ .. versionadded:: 1.2.0
+
+ :attr:`closing_transition` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `'out_sine'`.
"""
- Custom content.
- :attr:`screen` is an :class:`~kivy.properties.ObjectProperty`
- and defaults to `None`.
+ default_opening_height = NumericProperty(dp(200))
"""
+ Default opening height of the bottom sheet.
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self._gl_content.add_widget(self.screen)
- Clock.schedule_once(
- lambda x: self.resize_content_layout(self._gl_content, self.screen),
- 0,
- )
+ .. versionadded:: 1.2.0
+ :attr:`default_opening_height` is an :class:`~kivy.properties.NumericProperty`
+ and defaults to `dp(100)`.
+ """
-class MDListBottomSheet(MDBottomSheet):
- sheet_list = ObjectProperty()
+ duration_opening = NumericProperty(0.15)
"""
- :attr:`sheet_list` is an :class:`~kivy.properties.ObjectProperty`
- and defaults to `None`.
+ The duration of the bottom sheet opening animation.
+
+ :attr:`duration_opening` is an :class:`~kivy.properties.NumericProperty`
+ and defaults to `0.15`.
"""
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.sheet_list = SheetList(size_hint_y=None)
- self._gl_content.add_widget(self.sheet_list)
- Clock.schedule_once(
- lambda x: self.resize_content_layout(
- self._gl_content, self.sheet_list
- ),
- 0,
- )
+ duration_closing = NumericProperty(0.15)
+ """
+ The duration of the bottom sheet dialog closing animation.
- def add_item(self, text, callback, icon=None):
- """
- :arg text: element text;
- :arg callback: function that will be called when clicking on an item;
- :arg icon: which will be used as an icon to the left of the item;
- """
+ :attr:`duration_closing` is an :class:`~kivy.properties.NumericProperty`
+ and defaults to `0.15`.
+ """
- if icon:
- item = OneLineIconListItem(text=text, on_release=callback)
- item.add_widget(ListBottomSheetIconLeft(icon=icon))
- else:
- item = OneLineListItem(text=text, on_release=callback)
- item.bind(on_release=lambda x: self.dismiss())
- self.sheet_list.ids.box_sheet_list.add_widget(item)
+ animation = BooleanProperty(True)
+ """
+ Whether to use animation for opening and closing of the bottom sheet
+ or not.
+ :attr:`animation` is an :class:`~kivy.properties.BooleanProperty`
+ and defaults to `True`.
+ """
-class GridBottomSheetItem(ButtonBehavior, BoxLayout):
- source = StringProperty()
+ state = OptionProperty("close", options=["close", "open"])
"""
- Icon path if you use a local image or icon name
- if you use icon names from a file ``kivymd/icon_definitions.py``.
+ Menu state. Available options are: `'close'`, `'open'`.
+
+ .. versionadded:: 1.2.0
- :attr:`source` is an :class:`~kivy.properties.StringProperty`
- and defaults to `''`.
+ :attr:`state` is an :class:`~kivy.properties.OptionProperty`
+ and defaults to `'close'`.
"""
- caption = StringProperty()
+ scrim_layer_color = ColorProperty([0, 0, 0, 1])
"""
- Item text.
+ Color for scrim in (r, g, b, a) or string format.
- :attr:`caption` is an :class:`~kivy.properties.StringProperty`
- and defaults to `''`.
+ .. versionadded:: 1.2.0
+
+ :attr:`scrim_layer_color` is a :class:`~kivy.properties.ColorProperty`
+ and defaults to `[0, 0, 0, 1]`.
"""
- icon_size = NumericProperty("24sp")
+ bg_color = ColorProperty(None)
"""
- Icon size.
+ Background color of bottom sheet in (r, g, b, a) or string format.
- :attr:`caption` is an :class:`~kivy.properties.StringProperty`
- and defaults to `'24sp'`.
+ :attr:`bg_color` is an :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
"""
+ radius_from = OptionProperty(
+ None,
+ options=[
+ "top_left",
+ "top_right",
+ "top",
+ "bottom_right",
+ "bottom_left",
+ "bottom",
+ ],
+ allownone=True,
+ deprecated=True,
+ )
+ """
+ Sets which corners to cut from the dialog. Available options are:
+ `"top_left"`, `"top_right"`, `"top"`, `"bottom_right"`, `"bottom_left"`,
+ `"bottom"`.
-class MDGridBottomSheet(MDBottomSheet):
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.sheet_list = SheetList(size_hint_y=None)
- self.sheet_list.ids.box_sheet_list.cols = 3
- self.sheet_list.ids.box_sheet_list.padding = (dp(16), 0, dp(16), dp(96))
- self._gl_content.add_widget(self.sheet_list)
- Clock.schedule_once(
- lambda x: self.resize_content_layout(
- self._gl_content, self.sheet_list
- ),
- 0,
- )
+ .. deprecated:: 1.2.0
+ Use :attr:`radius` instead.
- def add_item(self, text, callback, icon_src):
+ :attr:`radius_from` is an :class:`~kivy.properties.OptionProperty`
+ and defaults to `None`.
+ """
+
+ value_transparent = ColorProperty([0, 0, 0, 0.8], deprecated=True)
+ """
+ Background color in (r, g, b, a) or string format transparency value when
+ opening a dialog.
+
+ .. deprecated:: 1.2.0
+
+ :attr:`value_transparent` is an :class:`~kivy.properties.ColorProperty`
+ and defaults to `[0, 0, 0, 0.8]`.
+ """
+
+ _diff_between_touch_height_sheet = 0
+ _alpha_channel_value = 0
+ # Menu state:
+ # - value 'down' - menu is captured;
+ # - value 'none' - menu is not captured;
+ _state = OptionProperty("none", options=["none", "down"])
+ # There was a touch to the bottom sheet.
+ _touch_sheet = False
+ # kivymd.uix.bottomsheet.bottomsheet.BottomSheetScrimLayer object.
+ _scrim_layer = ObjectProperty(None, allownone=True)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.y = -Window.height # start bottom sheet position
+ Clock.schedule_once(self.check_parent)
+ Clock.schedule_once(self.check_max_opening_height)
+ Clock.schedule_once(self.add_scrim_layer)
+ self.register_event_type("on_open")
+ self.register_event_type("on_close")
+ self.register_event_type("on_progress")
+
+ def on_progress(self, *args) -> None:
+ """Bottom sheet opening/closing progress event."""
+
+ def on_open(self, *args) -> None:
+ """Event when opening the bottom sheet."""
+
+ def on_close(self, *args) -> None:
+ """Event when closing the bottom sheet."""
+
+ def on_long_touch(self, touch, *args):
+ if self.ids.drag_handle_container.collide_point(touch.x, touch.y):
+ self._state = "down"
+
+ def on_touch_down(self, touch):
+ if self.type == "standard":
+ super().on_touch_down(touch)
+
+ if self.collide_point(touch.x, touch.y):
+ self._touch_sheet = not self._touch_sheet
+ if self.type == "standard":
+ return True
+ elif self.type == "modal":
+ return super().on_touch_down(touch)
+
+ def on_touch_up(self, touch):
+ self._diff_between_touch_height_sheet = 0
+ self._alpha_channel_value = 0
+
+ if self.collide_point(touch.x, touch.y):
+ self._touch_sheet = not self._touch_sheet
+ if self.auto_positioning:
+ if self._state == "down":
+ self._set_state(touch.y)
+ else:
+ if self._state == "down":
+ self._touch_sheet = not self._touch_sheet
+ self._set_state(touch.y)
+
+ def on_touch_move(self, touch):
+ if self._state == "down":
+ if not self._diff_between_touch_height_sheet:
+ self._diff_between_touch_height_sheet = (
+ abs(self.y) if self.y else self.height
+ ) - touch.y
+
+ # FIXME: the behavior of the drag handle looks strange:
+ # sometimes the bottom sheet is dragged as needed, and sometimes
+ # it's position does not correspond to the cursor coordinates.
+ y = -(
+ (self.height - touch.y)
+ - 0 # self._diff_between_touch_height_sheet
+ )
+
+ if y > 0:
+ self.y = 0
+ return
+ if self.max_opening_height and touch.y > self.max_opening_height:
+ self.y = -(self.height - self.max_opening_height)
+ return
+
+ self.y = y
+
+ if self._scrim_layer and self.type == "modal":
+ if not self._alpha_channel_value:
+ self._alpha_channel_value = (
+ self._scrim_layer.md_bg_color[-1] - touch.psy
+ )
+
+ self._scrim_layer.md_bg_color = self._scrim_layer.md_bg_color[
+ :-1
+ ] + [touch.psy + self._alpha_channel_value]
+
+ #
+ # if self.radius == [0.0, 0.0, 0.0, 0.0]:
+ # self.radius = [16, 16, 0, 0]
+
+ return super().on_touch_move(touch)
+
+ def on_type(self, *args) -> None:
+ self.add_scrim_layer()
+
+ def add_scrim_layer(self, *args) -> None:
+ """
+ Adds a scrim layer to the parent widget on which the bottom sheet
+ will be displayed.
"""
- :arg text: element text;
- :arg callback: function that will be called when clicking on an item;
- :arg icon_src: icon item;
+
+ if not self._scrim_layer and self.type == "modal":
+ self._scrim_layer = BottomSheetScrimLayer()
+ self.parent.add_widget(self._scrim_layer, index=1)
+ self._scrim_layer.bind(on_touch_down=self._on_touch_down_layer)
+ if self._scrim_layer and self.type == "standard":
+ self.parent.remove_widget(self._scrim_layer)
+ self._scrim_layer = None
+
+ def check_max_opening_height(self, *args) -> None:
+ if (
+ self.max_opening_height
+ and self.max_opening_height < self.default_opening_height
+ ):
+ raise ValueError(
+ "The value of `max_opening_height` cannot be less "
+ "than the value of `default_opening_height`"
+ )
+
+ def check_parent(self, *args) -> None:
+ """
+ Checks the type of parent widget to which the bottom sheet
+ will be added.
"""
- def tap_on_item(instance):
- callback(instance)
+ if not issubclass(self.parent.__class__, Screen):
+ raise TypeError(
+ f"The bottom sheet can only be added to the {Screen} "
+ f"or {MDScreen} widgets."
+ )
+
+ def dismiss(self, *args) -> None:
+ """Dismiss of bottom sheet."""
+
+ anim = Animation(
+ y=-self.height,
+ d=self.duration_closing if self.animation else 0,
+ t=self.closing_transition,
+ )
+ anim.bind(
+ on_complete=lambda x, y: self.dispatch("on_close"),
+ on_progress=lambda x, y, z: self.dispatch("on_progress", z),
+ )
+ anim.start(self)
+
+ # Animation(
+ # radius=[16, 16, 0, 0],
+ # d=self.duration_closing if self.animation else 0,
+ # ).start(self)
+
+ if self.type == "modal":
+ Animation(
+ md_bg_color=self.scrim_layer_color[:-1] + [0],
+ d=self.duration_closing if self.animation else 0,
+ ).start(self._scrim_layer)
+
+ self.state = "close"
+
+ def expand(self) -> None:
+ """Expand of bottom sheet."""
+
+ Animation(
+ y=0
+ if not self.max_opening_height
+ else -(self.height - self.default_opening_height),
+ d=self.duration_opening if self.animation else 0,
+ t=self.opening_transition,
+ ).start(self)
+
+ # Animation(
+ # radius=[0, 0, 0, 0],
+ # d=self.duration_opening if self.animation else 0,
+ # ).start(self)
+
+ def open(self, *args) -> None:
+ """Opening of bottom sheet."""
+
+ anim = Animation(
+ y=-(self.height - self.default_opening_height),
+ d=self.duration_opening if self.animation else 0,
+ t=self.opening_transition,
+ )
+ anim.bind(
+ on_complete=lambda x, y: self.dispatch("on_open"),
+ on_progress=lambda x, y, z: self.dispatch("on_progress", z),
+ )
+ anim.start(self)
+
+ if self.type == "modal":
+ alpha_channel_value = 100 / self.parent.height
+ Animation(
+ md_bg_color=self.scrim_layer_color[:-1] + [alpha_channel_value],
+ d=self.duration_opening if self.animation else 0,
+ ).start(self._scrim_layer)
+
+ self.state = "open"
+
+ def clear_content(self) -> None:
+ """Removes custom content from the bottom sheet."""
+
+ self.ids.container.clear_widgets()
+
+ def add_widget(self, widget, *args, **kwargs):
+ if isinstance(widget, MDBottomSheetDragHandle):
+ self.ids.drag_handle_container.add_widget(widget)
+ return
+ elif isinstance(widget, MDBottomSheetContent):
+ self.ids.container.add_widget(widget)
+ return
+ return super().add_widget(widget)
+
+ def _set_state(self, y):
+ self._state = "none"
+ if y < self.height / 2:
+ self.dismiss()
+ elif y > self.height / 2:
+ self.expand()
+
+ def _on_touch_down_layer(self, instance, touch):
+ if instance.collide_point(touch.x, touch.y):
+ if self._touch_sheet:
+ return True
+
+ if self.state == "open" and not self.auto_dismiss:
+ return True
+ elif self.state == "open" and self.auto_dismiss:
self.dismiss()
+ return True
+
+
+class MDCustomBottomSheet(MDBottomSheet):
+ """
+ .. deprecated:: 1.2.0
+ Use :class:`~kivymd.uix.bottomsheet.bottomsheet.MDBottomSheet`
+ class instead.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ Logger.warning(
+ "KivyMD: "
+ "The `MDCustomBottomSheet` class has been deprecated. "
+ "Use the `MDBottomSheet` class instead."
+ )
+
+
+class MDListBottomSheet(MDBottomSheet):
+ """
+ .. deprecated:: 1.2.0
+ Use :class:`~kivymd.uix.bottomsheet.bottomsheet.MDBottomSheet`
+ class instead.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ Logger.warning(
+ "KivyMD: "
+ "The `MDListBottomSheet` class has been deprecated. "
+ "Use the `MDBottomSheet` class instead."
+ )
+
+
+class MDGridBottomSheet(MDBottomSheet):
+ """
+ .. deprecated:: 1.2.0
+ Use :class:`~kivymd.uix.bottomsheet.bottomsheet.MDBottomSheet`
+ class instead.
+ """
- item = GridBottomSheetItem(
- caption=text, on_release=tap_on_item, source=icon_src
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ Logger.warning(
+ "KivyMD: "
+ "The `MDGridBottomSheet` class has been deprecated. "
+ "Use the `MDBottomSheet` class instead."
)
- if len(self._gl_content.children) % 3 == 0:
- self._gl_content.height += dp(96)
- self.sheet_list.ids.box_sheet_list.add_widget(item)

diff --git a/sbapp/kivymd/uix/boxlayout.py b/sbapp/kivymd/uix/boxlayout.py
index 6922de79..c5f3e75d 100644
--- a/sbapp/kivymd/uix/boxlayout.py
+++ b/sbapp/kivymd/uix/boxlayout.py
@@ -87,11 +87,14 @@ __all__ = ("MDBoxLayout",)
from kivy.uix.boxlayout import BoxLayout
+from kivymd.theming import ThemableBehavior
from kivymd.uix import MDAdaptiveWidget
from kivymd.uix.behaviors import DeclarativeBehavior
-class MDBoxLayout(DeclarativeBehavior, BoxLayout, MDAdaptiveWidget):
+class MDBoxLayout(
+ DeclarativeBehavior, ThemableBehavior, BoxLayout, MDAdaptiveWidget
+):
"""
Box layout class.

diff --git a/sbapp/kivymd/uix/button/button.kv b/sbapp/kivymd/uix/button/button.kv
index 4cee3390..77f7d09d 100644
--- a/sbapp/kivymd/uix/button/button.kv
+++ b/sbapp/kivymd/uix/button/button.kv
@@ -2,6 +2,7 @@
canvas:
Clear
Color:
+ group: "bg-color"
rgba:
self._md_bg_color \
if not self.disabled else \
@@ -12,6 +13,7 @@
source: self.source if hasattr(self, "source") else ""
radius: [root._radius, ]
Color:
+ group: "outline-color"
rgba:
root._line_color \
if not root.disabled else \
@@ -92,9 +94,11 @@
root.theme_cls.disabled_hint_text_color \
if not root.disabled_color else \
root.disabled_color
-
- on_icon:
- if self.icon not in md_icons.keys(): self.size_hint = (1, 1)
+ # Fix https://github.com/kivymd/KivyMD/issues/1448
+ # TODO: Perhaps this change may affect other widgets.
+ # You need to create tests.
+ # on_icon:
+ # if self.icon not in md_icons.keys(): self.size_hint = (1, 1)
theme_text_color: root._theme_icon_color

diff --git a/sbapp/kivymd/uix/button/button.py b/sbapp/kivymd/uix/button/button.py
index 0fac0e2e..5750a334 100755
--- a/sbapp/kivymd/uix/button/button.py
+++ b/sbapp/kivymd/uix/button/button.py
@@ -679,6 +679,15 @@ from kivy.weakproxy import WeakProxy
from kivymd import uix_path
from kivymd.color_definitions import text_colors
from kivymd.font_definitions import theme_font_styles
+from kivymd.material_resources import (
+ FLOATING_ACTION_BUTTON_M2_ELEVATION,
+ FLOATING_ACTION_BUTTON_M2_OFFSET,
+ FLOATING_ACTION_BUTTON_M3_ELEVATION,
+ FLOATING_ACTION_BUTTON_M3_OFFSET,
+ FLOATING_ACTION_BUTTON_M3_SOFTNESS,
+ RAISED_BUTTON_OFFSET,
+ RAISED_BUTTON_SOFTNESS,
+)
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import (
CommonElevationBehavior,
@@ -704,62 +713,6 @@ theme_text_color_options = (
"ContrastParentBackground",
)
-# FIXME: If you set a new elevation value for the button
-# (press the "Set elevation" button), then disable the button
-# (press the "Disabled" button), and then enable the button
-# (press the "Undisabled" button), then the previously set elevation value is
-# reset to zero.
-# In addition, if you set a new elevation value
-# (press the "Set elevation" button) and click on the button for which we set
-# the elevation value, then the new elevation value will receive the previous
-# elevation value. This problem is only related to the buttons.
-# For example, there is no such problem for the MDCard widget.
-
-"""
-from kivy.lang import Builder
-
-from kivymd.app import MDApp
-
-KV = '''
-MDScreen:
-
- MDRaisedButton:
- size_hint: .5, .5
- id: button
- pos_hint: {"center_x": .5, "center_y": .5}
- elevation: 0
-
- MDBoxLayout:
- adaptive_size: True
- pos_hint: {"center_x": .5}
- spacing: 12
- padding: 12
-
- MDRaisedButton:
- text: "Set elevation"
- pos_hint: {"center_x": .5, "bottom": 1}
- on_release: button.elevation = 4
-
- MDRaisedButton:
- text: "Disabled"
- pos_hint: {"center_x": .5, "bottom": 1}
- on_release: button.disabled = True
-
- MDRaisedButton:
- text: "Undisabled"
- pos_hint: {"center_x": .5, "bottom": 1}
- on_release: button.disabled = False
-'''
-
-
-class Test(MDApp):
- def build(self):
- return Builder.load_string(KV)
-
-
-Test().run()
-"""
-
class BaseButton(
DeclarativeBehavior,
@@ -772,7 +725,12 @@ class BaseButton(
Base class for all buttons.
For more information, see in the
- :class:`~kivy.uix.anchorlayout.AnchorLayout` class documentation.
+ :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and
+ :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~kivy.uix.anchorlayout.AnchorLayout`
+ classes documentation.
"""
padding = VariableListProperty([dp(16), dp(8), dp(16), dp(8)])
@@ -1208,7 +1166,7 @@ class ButtonElevationBehaviour(CommonElevationBehavior):
_elevation_raised = NumericProperty()
_anim_raised = ObjectProperty(None, allownone=True)
- _default_elevation = 3
+ _default_elevation = 2
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -1220,8 +1178,9 @@ class ButtonElevationBehaviour(CommonElevationBehavior):
self.on_disabled(self, self.disabled)
def create_anim_raised(self, *args) -> None:
- self._elevation_raised = self.elevation + 1.2
- self._anim_raised = Animation(elevation=self.elevation + 1, d=0.15)
+ if self.elevation:
+ self._elevation_raised = self.elevation
+ self._anim_raised = Animation(elevation=self.elevation + 1, d=0.15)
def on_touch_down(self, touch):
if not self.disabled:
@@ -1231,21 +1190,21 @@ class ButtonElevationBehaviour(CommonElevationBehavior):
return False
if self in touch.ud:
return False
- if self._anim_raised:
+ if self._anim_raised and self.elevation:
self._anim_raised.start(self)
return super().on_touch_down(touch)
def on_touch_up(self, touch):
if not self.disabled:
- if touch.grab_current is not self:
+ if self in touch.ud:
self.stop_elevation_anim()
return super().on_touch_up(touch)
- self.stop_elevation_anim()
return super().on_touch_up(touch)
def stop_elevation_anim(self):
Animation.cancel_all(self, "elevation")
- self.elevation = self._elevation_raised - 1
+ if self._anim_raised and self.elevation:
+ self.elevation = self._elevation_raised
class ButtonContentsText:
@@ -1313,6 +1272,10 @@ class MDFlatButton(BaseButton, ButtonContentsText):
"""
A flat rectangular button with (by default) no border or background.
Text is the default text color.
+
+ For more information, see in the
+ :class:`~BaseButton` and :class:`~ButtonContentsText`
+ classes documentation.
"""
padding = VariableListProperty([dp(8), dp(8), dp(8), dp(8)])
@@ -1334,6 +1297,12 @@ class MDRaisedButton(BaseButton, ButtonElevationBehaviour, ButtonContentsText):
"""
A flat button with (by default) a primary color fill and matching
color text.
+
+ For more information, see in the
+ :class:`~BaseButton` and
+ :class:`~ButtonElevationBehaviour` and
+ :class:`~ButtonContentsText`
+ classes documentation.
"""
# FIXME: Move the underlying attributes to the :class:`~BaseButton` class.
@@ -1345,15 +1314,19 @@ class MDRaisedButton(BaseButton, ButtonElevationBehaviour, ButtonContentsText):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.shadow_softness = 8
- self.shadow_offset = (0, 2)
- self.shadow_radius = self._radius * 2
+ self.shadow_softness = RAISED_BUTTON_SOFTNESS
+ self.shadow_offset = RAISED_BUTTON_OFFSET
+ # self.shadow_radius = self._radius * 2
class MDRectangleFlatButton(BaseButton, ButtonContentsText):
"""
A flat button with (by default) a primary color border and primary
color text.
+
+ For more information, see in the
+ :class:`~BaseButton` and :class:`~ButtonContentsText`
+ classes documentation.
"""
_default_line_color = None
@@ -1368,6 +1341,12 @@ class MDRectangleFlatIconButton(
"""
A flat button with (by default) a primary color border, primary color text
and a primary color icon on the left.
+
+ For more information, see in the
+ :class:`~BaseButton` and
+ :class:`~OldButtonIconMixin` and
+ :class:`~ButtonContentsIconText`
+ classes documentation.
"""
_default_line_color = None
@@ -1382,6 +1361,10 @@ class MDRoundFlatButton(BaseButton, ButtonContentsText):
"""
A flat button with (by default) fully rounded corners, a primary
color border and primary color text.
+
+ For more information, see in the
+ :class:`~BaseButton` and :class:`~ButtonContentsText`
+ classes documentation.
"""
_default_line_color = None
@@ -1400,6 +1383,12 @@ class MDRoundFlatIconButton(
"""
A flat button with (by default) rounded corners, a primary color border,
primary color text and a primary color icon on the left.
+
+ For more information, see in the
+ :class:`~BaseButton` and
+ :class:`~OldButtonIconMixin` and
+ :class:`~ButtonContentsIconText`
+ classes documentation.
"""
_default_line_color = None
@@ -1418,6 +1407,10 @@ class MDFillRoundFlatButton(BaseButton, ButtonContentsText):
"""
A flat button with (by default) rounded corners, a primary color fill
and primary color text.
+
+ For more information, see in the
+ :class:`~BaseButton` and :class:`~ButtonContentsText`
+ classes documentation.
"""
_default_md_bg_color = None
@@ -1436,6 +1429,12 @@ class MDFillRoundFlatIconButton(
"""
A flat button with (by default) rounded corners, a primary color fill,
primary color text and a primary color icon on the left.
+
+ For more information, see in the
+ :class:`~BaseButton` and
+ :class:`~OldButtonIconMixin` and
+ :class:`~ButtonContentsIconText`
+ classes documentation.
"""
_default_md_bg_color = None
@@ -1451,7 +1450,14 @@ class MDFillRoundFlatIconButton(
class MDIconButton(BaseButton, OldButtonIconMixin, ButtonContentsIcon):
- """A simple rounded icon button."""
+ """
+ A simple rounded icon button.
+
+ For more information, see in the
+ :class:`~BaseButton` and
+ :class:`~OldButtonIconMixin` and
+ :class:`~ButtonContentsIcon` classes documentation.
+ """
icon = StringProperty("checkbox-blank-circle")
"""
@@ -1489,6 +1495,12 @@ class MDFloatingActionButton(
Implementation
`FAB <https://m3.material.io/components/floating-action-button/overview>`_
button.
+
+ For more information, see in the
+ :class:`~BaseButton` and
+ :class:`~OldButtonIconMixin` and
+ :class:`~ButtonElevationBehaviour` and
+ :class:`~ButtonContentsIcon` classes documentation.
"""
type = OptionProperty("standard", options=["small", "large", "standard"])
@@ -1530,10 +1542,13 @@ class MDFloatingActionButton(
def set__radius(self, *args) -> None:
if self.theme_cls.material_style == "M2":
self.shadow_radius = self.height / 2
+ self.elevation = FLOATING_ACTION_BUTTON_M2_ELEVATION
+ self.shadow_offset = FLOATING_ACTION_BUTTON_M2_OFFSET
self.rounded_button = True
else:
- self.shadow_softness = 8
- self.shadow_offset = (0, 2)
+ self.shadow_softness = FLOATING_ACTION_BUTTON_M3_SOFTNESS
+ self.shadow_offset = FLOATING_ACTION_BUTTON_M3_OFFSET
+ self.elevation = FLOATING_ACTION_BUTTON_M3_ELEVATION
self.rounded_button = False
if self.type == "small":
@@ -1566,6 +1581,14 @@ class MDFloatingActionButton(
class MDTextButton(ButtonBehavior, MDLabel):
+ """
+ Text button class.
+
+ For more information, see in the
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~kivymd.uix.label.MDLabel` classes documentation.
+ """
+
color = ColorProperty(None)
"""
Button color in (r, g, b, a) or string format.
@@ -1638,6 +1661,12 @@ class MDFloatingActionButtonSpeedDial(
For more information, see in the
:class:`~kivy.uix.floatlayout.FloatLayout` class documentation.
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivy.uix.floatlayout.FloatLayout`
+ lasses documentation.
+
:Events:
:attr:`on_open`
Called when a stack is opened.
@@ -1868,7 +1897,7 @@ class MDFloatingActionButtonSpeedDial(
"""
Background color of root button in (r, g, b, a) or string format.
- .. code-clock:: kv
+ .. code-block:: kv
MDFloatingActionButtonSpeedDial:
bg_color_root_button: "red"
@@ -1884,7 +1913,7 @@ class MDFloatingActionButtonSpeedDial(
"""
Background color of the stack buttons in (r, g, b, a) or string format.
- .. code-clock:: kv
+ .. code-block:: kv
MDFloatingActionButtonSpeedDial:
bg_color_root_button: "red"
@@ -1901,7 +1930,7 @@ class MDFloatingActionButtonSpeedDial(
"""
The color icon of the stack buttons in (r, g, b, a) or string format.
- .. code-clock:: kv
+ .. code-block:: kv
MDFloatingActionButtonSpeedDial:
bg_color_root_button: "red"
@@ -1919,7 +1948,7 @@ class MDFloatingActionButtonSpeedDial(
"""
The color icon of the root button in (r, g, b, a) or string format.
- .. code-clock:: kv
+ .. code-block:: kv
MDFloatingActionButtonSpeedDial:
bg_color_root_button: "red"
@@ -1939,7 +1968,7 @@ class MDFloatingActionButtonSpeedDial(
Background color for the floating text of the buttons in (r, g, b, a)
or string format.
- .. code-clock:: kv
+ .. code-block:: kv
MDFloatingActionButtonSpeedDial:
bg_hint_color: "red"

diff --git a/sbapp/kivymd/uix/card/card.py b/sbapp/kivymd/uix/card/card.py
index d224ae18..7e0bdf9a 100755
--- a/sbapp/kivymd/uix/card/card.py
+++ b/sbapp/kivymd/uix/card/card.py
@@ -94,6 +94,7 @@ An example of the implementation of a card in the style of material design versi
style=style,
text=style.capitalize(),
md_bg_color=styles[style],
+ shadow_offset=(0, -1),
)
)
@@ -152,10 +153,9 @@ An example of the implementation of a card in the style of material design versi
),
line_color=(0.2, 0.2, 0.2, 0.8),
style=style,
- padding="4dp",
- size_hint=(None, None),
- size=("200dp", "100dp"),
+ text=style.capitalize(),
md_bg_color=styles[style],
+ shadow_offset=(0, -1),
)
)
@@ -699,7 +699,12 @@ from kivy.utils import get_color_from_hex
from kivymd import uix_path
from kivymd.color_definitions import colors
+from kivymd.material_resources import (
+ CARD_STYLE_ELEVATED_M3_ELEVATION,
+ CARD_STYLE_OUTLINED_FILLED_M3_ELEVATION,
+)
from kivymd.theming import ThemableBehavior
+from kivymd.uix import MDAdaptiveWidget
from kivymd.uix.behaviors import (
BackgroundColorBehavior,
CommonElevationBehavior,
@@ -716,12 +721,17 @@ with open(
Builder.load_string(kv_file.read())
-class MDSeparator(ThemableBehavior, MDBoxLayout):
- """A separator line."""
+class MDSeparator(MDBoxLayout):
+ """
+ A separator line.
+
+ For more information, see in the
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation.
+ """
color = ColorProperty(None)
"""
- Separator color.
+ Separator color in (r, g, b, a) or string format.
:attr:`color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
@@ -743,6 +753,7 @@ class MDSeparator(ThemableBehavior, MDBoxLayout):
class MDCard(
DeclarativeBehavior,
+ MDAdaptiveWidget,
ThemableBehavior,
BackgroundColorBehavior,
RectangularRippleBehavior,
@@ -750,6 +761,21 @@ class MDCard(
FocusBehavior,
BoxLayout,
):
+ """
+ Card class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and
+ :class:`~kivymd.uix.MDAdaptiveWidget` and
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivymd.uix.behaviors.BackgroundColorBehavior` and
+ :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and
+ :class:`~kivymd.uix.behaviors.CommonElevationBehavior` and
+ :class:`~kivymd.uix.behaviors.FocusBehavior` and
+ :class:`~kivy.uix.boxlayout.BoxLayout` and
+ classes documentation.
+ """
+
focus_behavior = BooleanProperty(False)
"""
Using focus when hovering over a card.
@@ -824,9 +850,9 @@ class MDCard(
def set_elevation(self) -> None:
if self.theme_cls.material_style == "M3":
if self.style == "outlined" or self.style == "filled":
- self.elevation = 0
+ self.elevation = CARD_STYLE_OUTLINED_FILLED_M3_ELEVATION
elif self.style == "elevated":
- self.elevation = 2
+ self.elevation = CARD_STYLE_ELEVATED_M3_ELEVATION
def set_radius(self) -> None:
if (
@@ -848,6 +874,11 @@ class MDCard(
class MDCardSwipe(MDRelativeLayout):
"""
+ Card swipe class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.relativelayout.MDRelativeLayout` class documentation.
+
:Events:
:attr:`on_swipe_complete`
Called when a swipe of card is completed.
@@ -1065,7 +1096,11 @@ class MDCardSwipe(MDRelativeLayout):
class MDCardSwipeFrontBox(MDCard):
- pass
+ """
+ Card swipe front box.
+
+ For more information, see in the :class:`~MDCard` class documentation.
+ """
class MDCardSwipeLayerBox(MDBoxLayout):

diff --git a/sbapp/kivymd/uix/carousel.py b/sbapp/kivymd/uix/carousel.py
index 4b56dbae..5f68886e 100644
--- a/sbapp/kivymd/uix/carousel.py
+++ b/sbapp/kivymd/uix/carousel.py
@@ -57,10 +57,11 @@ MDCarousel
from kivy.animation import Animation
from kivy.uix.carousel import Carousel
+from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import DeclarativeBehavior
-class MDCarousel(DeclarativeBehavior, Carousel):
+class MDCarousel(DeclarativeBehavior, ThemableBehavior, Carousel):
"""
based on kivy's carousel.

diff --git a/sbapp/kivymd/uix/chip/__init__.py b/sbapp/kivymd/uix/chip/__init__.py
index ec6050bd..fc6a3a29 100644
--- a/sbapp/kivymd/uix/chip/__init__.py
+++ b/sbapp/kivymd/uix/chip/__init__.py
@@ -1 +1 @@
-from .chip import MDChip # NOQA F401
+from .chip import MDChip, MDChipText # NOQA F401

diff --git a/sbapp/kivymd/uix/chip/chip.kv b/sbapp/kivymd/uix/chip/chip.kv
index 361bc98c..e008dd25 100644
--- a/sbapp/kivymd/uix/chip/chip.kv
+++ b/sbapp/kivymd/uix/chip/chip.kv
@@ -1,110 +1,38 @@
-<MDScalableCheckIcon>
- scale_value_x: 0
- scale_value_y: 0
- scale_value_z: 0
-
-
<MDChip>
size_hint_y: None
height: "32dp"
- spacing: "8dp"
adaptive_width: True
- radius: 16 if self.radius == [0, 0, 0, 0] else self.radius
- padding:
- "12dp" if not self.icon_left else "4dp", \
- 0, \
- "12dp" if not self.icon_right else "8dp", \
- 0
+ radius:
+ 16 \
+ if self.radius == [0, 0, 0, 0] else \
+ (max(self.radius) if max(self.radius) < self.height / 2 else 16)
md_bg_color:
+ ( \
( \
app.theme_cls.bg_darkest \
if app.theme_cls.theme_style == "Light" else \
app.theme_cls.bg_light \
) \
- if not self.disabled else app.theme_cls.disabled_hint_text_color
-
- canvas.before:
- Color:
- rgba:
- self.line_color \
- if not self.disabled else \
- app.theme_cls.disabled_hint_text_color
- Line:
- width: 1
- rounded_rectangle:
- ( \
- self.x, \
- self.y, \
- self.width, \
- self.height, \
- *self.radius, \
- self.height \
- )
-
- MDRelativeLayout:
- id: relative_box
- size_hint: None, None
- size: ("24dp", "24dp") if root.icon_left else (0, 0)
- pos_hint: {"center_y": .5}
- radius: [int(self.height / 2),]
-
- MDIcon:
- id: icon_left
- icon: root.icon_left
- size_hint: None, None
- size: ("28dp", "28dp") if root.icon_left else (0, 0)
- theme_text_color: "Custom"
- pos_hint: {"center_y": .5}
- pos: 0, -2
- text_color:
- ( \
- root.icon_left_color \
- if root.icon_left_color else \
- root.theme_cls.disabled_hint_text_color \
- ) \
- if not self.disabled else app.theme_cls.disabled_hint_text_color
-
- MDBoxLayout:
- id: icon_left_box
- size_hint: None, None
- radius: [int(self.height / 2),]
- size: ("28dp", "28dp") if root.icon_left else (0, 0)
- pos: 0, -2
-
- MDScalableCheckIcon:
- id: check_icon
- icon: "check"
- size_hint: None, None
- size: "28dp", "28dp"
- color: (1, 1, 1, 1) if not root.icon_check_color else root.icon_check_color
- pos: 2, -2
-
- MDLabel:
- id: label
- text: root.text
- adaptive_size: True
- markup: True
- pos_hint: {"center_y": .5}
- color:
- ( \
- root.text_color \
- if root.text_color else \
- root.theme_cls.disabled_hint_text_color \
- ) \
- if not self.disabled else app.theme_cls.disabled_hint_text_color
-
- MDIcon:
- id: icon_right
- icon: root.icon_right
- size_hint: None, None
- size: ("18dp", "18dp") if root.icon_right else (0, 0)
- font_size: "18sp" if root.icon_right else 0
- theme_text_color: "Custom"
- pos_hint: {"center_y": .5}
- text_color:
- ( \
- root.icon_right_color \
- if root.icon_right_color else \
- root.theme_cls.disabled_hint_text_color \
- ) \
- if not self.disabled else app.theme_cls.disabled_hint_text_color
+ if not self._origin_md_bg_color else \
+ self._origin_md_bg_color
+ ) \
+ if not self.disabled else app.theme_cls.disabled_primary_color
+ line_color:
+ app.theme_cls.disabled_hint_text_color \
+ if self.disabled else ( \
+ self._origin_line_color \
+ if self._origin_line_color else \
+ self.line_color \
+ )
+
+ LeadingIconContainer:
+ id: leading_icon_container
+ adaptive_width: True
+
+ LabelTextContainer:
+ id: label_container
+ adaptive_width: True
+
+ TrailingIconContainer:
+ id: trailing_icon_container
+ adaptive_width: True

diff --git a/sbapp/kivymd/uix/chip/chip.py b/sbapp/kivymd/uix/chip/chip.py
index 03e1a98d..c9f35931 100755
--- a/sbapp/kivymd/uix/chip/chip.py
+++ b/sbapp/kivymd/uix/chip/chip.py
@@ -4,9 +4,12 @@ Components/Chip
.. seealso::
- `Material Design spec, Chips <https://material.io/components/chips>`_
+ `Material Design spec, Chips <https://m3.material.io/components/chips/overview>`_
-.. rubric:: Chips are compact elements that represent an input, attribute, or action.
+.. rubric:: Chips can show multiple interactive elements together in the same
+ area, such as a list of selectable movie times, or a series of email
+ contacts. There are four types of chips: assist, filter, input, and
+ suggestion.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chips.png
:align: center
@@ -14,6 +17,122 @@ Components/Chip
Usage
-----
+.. tabs::
+
+ .. tab:: Declarative KV style
+
+ .. code-block:: python
+
+ from kivy.lang import Builder
+
+ from kivymd.app import MDApp
+
+ KV = '''
+ MDScreen:
+
+ MDChip:
+ pos_hint: {"center_x": .5, "center_y": .5}
+
+ MDChipText:
+ text: "MDChip"
+ '''
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ return Builder.load_string(KV)
+
+
+ Example().run()
+
+ .. tab:: Declarative Python style
+
+ .. code-block:: python
+
+ from kivymd.app import MDApp
+ from kivymd.uix.chip import MDChip, MDChipText
+ from kivymd.uix.screen import MDScreen
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ return (
+ MDScreen(
+ MDChip(
+ MDChipText(
+ text="MDChip"
+ ),
+ pos_hint={"center_x": .5, "center_y": .5},
+ )
+ )
+ )
+
+
+ Example().run()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip.png
+ :align: center
+
+Anatomy
+-------
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/anatomy-chip.png
+ :align: center
+
+1. Container
+2. Label text
+3. Leading icon or image (optional)
+4. Trailing remove icon (optional, input & filter chips only)
+
+Container
+---------
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/radius-chip.png
+ :align: center
+
+All chips are slightly rounded with an 8dp corner.
+
+Shadows and elevation
+---------------------
+
+Chip containers can be elevated if the placement requires protection, such as
+on top of an image.
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadows-elevation-chip.png
+ :align: center
+
+The following types of chips are available:
+-------------------------------------------
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/available-type-chips.png
+ :align: center
+
+- Assist_
+- Filter_
+- Input_
+- Suggestion_
+
+.. Assist:
+Assist
+------
+
+`Assist chips <https://m3.material.io/components/chips/guidelines#5dd1928c-1476-4029-bdc5-fde66fc0dcb1>`_
+represent smart or automated actions that can span multiple apps, such as
+opening a calendar event from the home screen. Assist chips function as
+though the user asked an assistant to complete the action. They should appear
+dynamically and contextually in a UI.
+
+An alternative to assist chips are buttons, which should appear persistently
+and consistently.
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/assist-chip.png
+ :align: center
+
+Example of assist
+-----------------
+
.. code-block:: python
from kivy.lang import Builder
@@ -21,305 +140,606 @@ Usage
from kivymd.app import MDApp
KV = '''
+ <CommonLabel@MDLabel>
+ adaptive_size: True
+ theme_text_color: "Custom"
+ text_color: "#e6e9df"
+
+
+ <CommonAssistChip@MDChip>
+ # Custom attribute.
+ text: ""
+ icon: ""
+
+ # Chip attribute.
+ type: "assist"
+ md_bg_color: "#2a3127"
+ line_color: "grey"
+ elevation: 1
+ shadow_softness: 2
+
+ MDChipLeadingIcon:
+ icon: root.icon
+ theme_text_color: "Custom"
+ text_color: "#68896c"
+
+ MDChipText:
+ text: root.text
+ theme_text_color: "Custom"
+ text_color: "#e6e9df"
+
+
MDScreen:
- MDChip:
- text: "Portland"
- pos_hint: {"center_x": .5, "center_y": .5}
- on_release: app.on_release_chip(self)
+ FitImage:
+ source: "bg.png"
+
+ MDBoxLayout:
+ orientation: "vertical"
+ adaptive_size: True
+ pos_hint: {"center_y": .6, "center_x": .5}
+
+ CommonLabel:
+ text: "in 10 mins"
+ bold: True
+ pos_hint: {"center_x": .5}
+
+ CommonLabel:
+ text: "Therapy with Thea"
+ font_style: "H3"
+ padding_y: "12dp"
+
+ CommonLabel:
+ text: "Video call"
+ font_style: "H5"
+ pos_hint: {"center_x": .5}
+
+ MDBoxLayout:
+ adaptive_size: True
+ pos_hint: {"center_x": .5}
+ spacing: "12dp"
+ padding: 0, "24dp", 0, 0
+
+ CommonAssistChip:
+ text: "Home office"
+ icon: "map-marker"
+
+ CommonAssistChip:
+ text: "Chat"
+ icon: "message"
+
+ MDWidget:
'''
- class Test(MDApp):
+ class Example(MDApp):
def build(self):
+ self.theme_cls.primary_palette = "Teal"
+ self.theme_cls.theme_style = "Dark"
return Builder.load_string(KV)
- def on_release_chip(self, instance_check):
- print(instance_check)
-
- Test().run()
+ Example().run()
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ordinary-chip.png
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/example-assist-chip.png
:align: center
-Use with right icon
--------------------
+.. Filter:
+Filter
+------
-.. code-block:: kv
+`Filter chips <https://m3.material.io/components/chips/guidelines#8d453d50-8d8e-43aa-9ae3-87ed134d2e64>`_
+use tags or descriptive words to filter content. They can be a good alternative
+to toggle buttons or checkboxes.
- MDChip:
- text: "Portland"
- icon_right: "close-circle-outline"
+Tapping on a filter chip activates it and appends a leading checkmark icon to
+the starting edge of the chip label.
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-right-icon.png
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/filter-chip.png
:align: center
-Use with left icon
-------------------
+Example of filtering
+--------------------
-.. code-block:: kv
+.. code-block:: python
- MDChip:
- text: "Portland"
- icon_left: "map-marker"
+ from kivy.lang import Builder
+ from kivy.properties import StringProperty, ListProperty
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-left-icon.png
- :align: center
+ from kivymd.app import MDApp
+ from kivymd.uix.chip import MDChip, MDChipText
+ from kivymd.uix.list import OneLineIconListItem
+ from kivymd.icon_definitions import md_icons
+ from kivymd.uix.screen import MDScreen
+ from kivymd.utils import asynckivy
-Use with custom left icon
--------------------------
+ Builder.load_string(
+ '''
+ <CustomOneLineIconListItem>
-.. code-block:: kv
+ IconLeftWidget:
+ icon: root.icon
- MDChip:
- text: "Portland"
- icon_left: "avatar.png"
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-custom-left-icon.png
- :align: center
+ <PreviewIconsScreen>
-Use with left and right icon
-----------------------------
+ MDBoxLayout:
+ orientation: "vertical"
+ spacing: "14dp"
+ padding: "20dp"
-.. code-block:: kv
+ MDTextField:
+ id: search_field
+ hint_text: "Search icon"
+ mode: "rectangle"
+ icon_left: "magnify"
+ on_text: root.set_list_md_icons(self.text, True)
- MDChip:
- text: "Portland"
- icon_left: "avatar.png"
- icon_right: "close-circle-outline"
+ MDBoxLayout:
+ id: chip_box
+ spacing: "12dp"
+ adaptive_height: True
+
+ RecycleView:
+ id: rv
+ viewclass: "CustomOneLineIconListItem"
+ key_size: "height"
+
+ RecycleBoxLayout:
+ padding: dp(10)
+ default_size: None, dp(48)
+ default_size_hint: 1, None
+ size_hint_y: None
+ height: self.minimum_height
+ orientation: "vertical"
+ '''
+ )
+
+
+ class CustomOneLineIconListItem(OneLineIconListItem):
+ icon = StringProperty()
+
+
+ class PreviewIconsScreen(MDScreen):
+ filter = ListProperty() # list of tags for filtering icons
+
+ def set_filter_chips(self):
+ '''Asynchronously creates and adds chips to the container.'''
+
+ async def set_filter_chips():
+ for tag in ["Outline", "Off", "On"]:
+ await asynckivy.sleep(0)
+ chip = MDChip(
+ MDChipText(
+ text=tag,
+ ),
+ type="filter",
+ md_bg_color="#303A29",
+ )
+ chip.bind(active=lambda x, y, z=tag: self.set_filter(y, z))
+ self.ids.chip_box.add_widget(chip)
+
+ asynckivy.start(set_filter_chips())
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-left-right-icon.png
- :align: center
+ def set_filter(self, active: bool, tag: str) -> None:
+ '''Sets a list of tags for filtering icons.'''
-Use with outline
-----------------
+ if active:
+ self.filter.append(tag)
+ else:
+ self.filter.remove(tag)
+
+ def set_list_md_icons(self, text="", search=False) -> None:
+ '''Builds a list of icons.'''
-.. code-block:: kv
+ def add_icon_item(name_icon):
+ self.ids.rv.data.append(
+ {
+ "icon": name_icon,
+ "text": name_icon,
+ }
+ )
- MDChip:
- text: "Portland"
- icon_left: "avatar.png"
- icon_right: "close-circle-outline"
- line_color: app.theme_cls.disabled_hint_text_color
+ self.ids.rv.data = []
+ for name_icon in md_icons.keys():
+ for tag in self.filter:
+ if tag.lower() in name_icon:
+ if search:
+ if text in name_icon:
+ add_icon_item(name_icon)
+ else:
+ add_icon_item(name_icon)
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-outline.png
- :align: center
-Use with custom color
----------------------
+ class Example(MDApp):
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.screen = PreviewIconsScreen()
+
+ def build(self) -> PreviewIconsScreen:
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "LightGreen"
+ return self.screen
-.. code-block:: kv
+ def on_start(self) -> None:
+ self.screen.set_list_md_icons()
+ self.screen.set_filter_chips()
- MDChip:
- text: "Portland"
- icon_left: "avatar.png"
- icon_right: "close-circle-outline"
- line_color: app.theme_cls.disabled_hint_text_color
- md_bg_color: 1, 0, 0, .5
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-custom-color.png
+ Example().run()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/example-filtering-icons-chip.gif
:align: center
-Use with elevation
-------------------
+Tap a chip to select it. Multiple chips can be selected or unselected:
-.. code-block:: kv
+.. code-block:: python
- MDChip:
- text: "Portland"
- icon_left: "avatar.png"
- icon_right: "close-circle-outline"
- line_color: app.theme_cls.disabled_hint_text_color
- md_bg_color: 1, 0, 0, .5
- elevation: 4
+ from kivy.lang import Builder
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-elevation.png
- :align: center
+ from kivymd.app import MDApp
+ from kivymd.uix.chip import MDChip, MDChipText
+ from kivymd.uix.screen import MDScreen
+ from kivymd.utils import asynckivy
-Behavior
-========
+ Builder.load_string(
+ '''
+ <ChipScreen>
-Long press on the chip, it will be marked.
-When you click on the marked chip, the mark will be removed:
+ MDBoxLayout:
+ orientation: "vertical"
+ spacing: "14dp"
+ padding: "20dp"
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-activate.gif
- :align: center
+ MDLabel:
+ adaptive_height: True
+ text: "Select Type"
+
+ MDStackLayout:
+ id: chip_box
+ spacing: "12dp"
+ adaptive_height: True
+
+ MDWidget:
+
+ MDFlatButton:
+ text: "Uncheck chips"
+ pos: "20dp", "20dp"
+ on_release: root.unchecks_chips()
+ '''
+ )
+
+
+ class ChipScreen(MDScreen):
+ async def create_chips(self):
+ '''Asynchronously creates and adds chips to the container.'''
+
+ for tag in ["Extra Soft", "Soft", "Medium", "Hard"]:
+ await asynckivy.sleep(0)
+ self.ids.chip_box.add_widget(
+ MDChip(
+ MDChipText(
+ text=tag,
+ ),
+ type="filter",
+ md_bg_color="#303A29",
+ active=True,
+ )
+ )
+
+ def unchecks_chips(self) -> None:
+ '''Removes marks from all chips.'''
-Examples
-========
+ for chip in self.ids.chip_box.children:
+ if chip.active:
+ chip.active = False
-Multiple choose
----------------
-Selecting a single choice chip automatically deselects all other chips in the set.
+ class Example(MDApp):
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.screen = ChipScreen()
+
+ def build(self) -> ChipScreen:
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "LightGreen"
+ return self.screen
+
+ def on_start(self) -> None:
+ asynckivy.start(self.screen.create_chips())
+
+
+ Example().run()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/example-filtering-icons-chip-2.gif
+ :align: center
+
+Alternatively, a single chip can be selected.
+This offers an alternative to toggle buttons, radio buttons, or single select
+menus:
.. code-block:: python
- from kivy.animation import Animation
from kivy.lang import Builder
- from kivymd.uix.screen import MDScreen
- from kivymd.uix.chip import MDChip
from kivymd.app import MDApp
+ from kivymd.uix.chip import MDChip, MDChipText
+ from kivymd.uix.screen import MDScreen
+ from kivymd.utils import asynckivy
- KV = '''
- <MyScreen>
+ Builder.load_string(
+ '''
+ <ChipScreen>
MDBoxLayout:
orientation: "vertical"
- adaptive_size: True
- spacing: "12dp"
- padding: "56dp"
- pos_hint: {"center_x": .5, "center_y": .5}
+ spacing: "14dp"
+ padding: "20dp"
MDLabel:
- text: "Multiple choice"
- bold: True
- font_style: "H5"
- adaptive_size: True
+ adaptive_height: True
+ text: "Select Type"
- MDBoxLayout:
+ MDStackLayout:
id: chip_box
- adaptive_size: True
- spacing: "8dp"
+ spacing: "12dp"
+ adaptive_height: True
- MyChip:
- text: "Elevator"
- on_press: if self.active: root.removes_marks_all_chips()
+ MDFillRoundFlatButton:
+ text: "Add to cart"
+ md_bg_color: "green"
+ size_hint_x: 1
- MyChip:
- text: "Washer / Dryer"
- on_press: if self.active: root.removes_marks_all_chips()
+ MDWidget:
+ '''
+ )
- MyChip:
- text: "Fireplace"
- on_press: if self.active: root.removes_marks_all_chips()
+ class ChipScreen(MDScreen):
+ async def create_chips(self):
+ '''Asynchronously creates and adds chips to the container.'''
- ScreenManager:
+ for tag in ["Extra Soft", "Soft", "Medium", "Hard"]:
+ await asynckivy.sleep(0)
+ chip = MDChip(
+ MDChipText(
+ text=tag,
+ ),
+ type="filter",
+ md_bg_color="#303A29",
- MyScreen:
- '''
+ )
+ chip.bind(active=self.uncheck_chip)
+ self.ids.chip_box.add_widget(chip)
+ def uncheck_chip(self, current_chip: MDChip, active: bool) -> None:
+ '''Removes a mark from an already marked chip.'''
- class MyChip(MDChip):
- icon_check_color = (0, 0, 0, 1)
- text_color = (0, 0, 0, 0.5)
- _no_ripple_effect = True
+ if active:
+ for chip in self.ids.chip_box.children:
+ if current_chip is not chip:
+ if chip.active:
+ chip.active = False
+
+ class Example(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
- self.bind(active=self.set_chip_bg_color)
- self.bind(active=self.set_chip_text_color)
+ self.screen = ChipScreen()
- def set_chip_bg_color(self, instance_chip, active_value: int):
- '''
- Will be called every time the chip is activated/deactivated.
- Sets the background color of the chip.
- '''
+ def build(self) -> ChipScreen:
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "LightGreen"
+ return self.screen
- self.md_bg_color = (
- (0, 0, 0, 0.4)
- if active_value
- else (
- self.theme_cls.bg_darkest
- if self.theme_cls.theme_style == "Light"
- else (
- self.theme_cls.bg_light
- if not self.disabled
- else self.theme_cls.disabled_hint_text_color
- )
- )
- )
+ def on_start(self) -> None:
+ asynckivy.start(self.screen.create_chips())
- def set_chip_text_color(self, instance_chip, active_value: int):
- Animation(
- color=(0, 0, 0, 1) if active_value else (0, 0, 0, 0.5), d=0.2
- ).start(self.ids.label)
+ Example().run()
- class MyScreen(MDScreen):
- def removes_marks_all_chips(self):
- for instance_chip in self.ids.chip_box.children:
- if instance_chip.active:
- instance_chip.active = False
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/example-filtering-single-select.gif
+ :align: center
+.. Input:
+Input
+-----
- class Test(MDApp):
+`Input chips <https://m3.material.io/components/chips/guidelines#4d2d5ef5-3fcd-46e9-99f2-067747b2393f>`_
+represent discrete pieces of information entered by a user, such as Gmail
+contacts or filter options within a search field.
+
+They enable user input and verify that input by converting text into chips.
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/input-chip.png
+ :align: center
+
+Example of input
+----------------
+
+.. code-block:: python
+
+ from kivy.lang import Builder
+
+ from kivymd.app import MDApp
+
+ KV = '''
+ MDScreen:
+
+ MDChip:
+ pos_hint: {"center_x": .5, "center_y": .5}
+ type: "input"
+ line_color: "grey"
+ _no_ripple_effect: True
+
+ MDChipLeadingAvatar:
+ source: "data/logo/kivy-icon-128.png"
+
+ MDChipText:
+ text: "MDChip"
+
+ MDChipTrailingIcon:
+ icon: "close"
+ '''
+
+
+ class Example(MDApp):
def build(self):
+ self.theme_cls.theme_style = "Dark"
return Builder.load_string(KV)
- Test().run()
+ Example().run()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/example-input-chip.png
+ :align: center
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-multiple-choose.gif
+.. Suggestion:
+Suggestion
+----------
+
+`Suggestion chips <https://m3.material.io/components/chips/guidelines#36d7bb16-a9bf-4cf6-a73d-8e05510d66a7>`_
+help narrow a user’s intent by presenting dynamically generated suggestions,
+such as possible responses or search filters.
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/suggestion-chip.png
:align: center
-Only choose
------------
+Example of suggestion
+---------------------
-Only one chip will be selected.
+.. code-block::
+
+ from kivy.lang import Builder
+
+ from kivymd.app import MDApp
+
+ KV = '''
+ MDScreen:
+
+ MDChip:
+ pos_hint: {"center_x": .5, "center_y": .5}
+ type: "suggestion"
+ line_color: "grey"
+
+ MDChipText:
+ text: "MDChip"
+ '''
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ return Builder.load_string(KV)
+
+
+ Example().run()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/example-suggestion.png
+ :align: center
+
+API break
+=========
+
+1.1.1 version
+-------------
.. code-block:: python
+ from kivy.lang import Builder
+
+ from kivymd.app import MDApp
+
KV = '''
- <MyScreen>
+ MDScreen:
- [...]
+ MDChip:
+ text: "Portland"
+ pos_hint: {"center_x": .5, "center_y": .5}
+ on_release: app.on_release_chip(self)
+ '''
- MDBoxLayout:
- id: chip_box
- adaptive_size: True
- spacing: "8dp"
- MyChip:
- text: "Elevator"
- on_active: if self.active: root.removes_marks_all_chips(self)
+ class Test(MDApp):
+ def build(self):
+ return Builder.load_string(KV)
+
+ def on_release_chip(self, instance_check):
+ print(instance_check)
- MyChip:
- text: "Washer / Dryer"
- on_active: if self.active: root.removes_marks_all_chips(self)
- MyChip:
- text: "Fireplace"
- on_active: if self.active: root.removes_marks_all_chips(self)
+ Test().run()
+
+1.2.0 version
+-------------
+
+.. code-block:: python
+ from kivy.lang import Builder
- [...]
+ from kivymd.app import MDApp
+
+ KV = '''
+ MDScreen:
+
+ MDChip:
+ pos_hint: {"center_x": .5, "center_y": .5}
+ line_color: "grey"
+ on_release: app.on_release_chip(self)
+
+ MDChipText:
+ text: "MDChip"
'''
- class MyScreen(MDScreen):
- def removes_marks_all_chips(self, selected_instance_chip):
- for instance_chip in self.ids.chip_box.children:
- if instance_chip != selected_instance_chip:
- instance_chip.active = False
+ class Example(MDApp):
+ def build(self):
+ return Builder.load_string(KV)
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-only-choose.gif
- :align: center
+ def on_release_chip(self, instance_check):
+ print(instance_check)
+
+
+ Example().run()
"""
-__all__ = ("MDChip",)
+from __future__ import annotations
+
+__all__ = (
+ "MDChip",
+ "MDChipLeadingAvatar",
+ "MDChipLeadingIcon",
+ "MDChipTrailingIcon",
+ "MDChipText",
+)
import os
+from kivy import Logger
from kivy.animation import Animation
+from kivy.clock import Clock
from kivy.lang import Builder
from kivy.metrics import dp
-from kivy.properties import BooleanProperty, ColorProperty, StringProperty
+from kivy.properties import (
+ BooleanProperty,
+ ColorProperty,
+ OptionProperty,
+ StringProperty,
+ VariableListProperty,
+)
from kivy.uix.behaviors import ButtonBehavior
from kivymd import uix_path
-from kivymd.theming import ThemableBehavior
+from kivymd.material_resources import DEVICE_TYPE
from kivymd.uix.behaviors import (
+ CircularRippleBehavior,
CommonElevationBehavior,
RectangularRippleBehavior,
ScaleBehavior,
TouchBehavior,
)
from kivymd.uix.boxlayout import MDBoxLayout
-from kivymd.uix.label import MDIcon
+from kivymd.uix.label import MDIcon, MDLabel
with open(
os.path.join(uix_path, "chip", "chip.kv"), encoding="utf-8"
@@ -327,73 +747,206 @@ with open(
Builder.load_string(kv_file.read())
+class BaseChipIcon(
+ CircularRippleBehavior, ScaleBehavior, ButtonBehavior, MDIcon
+):
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.ripple_scale = 1.5
+ Clock.schedule_once(self.adjust_icon_size)
+
+ def adjust_icon_size(self, *args) -> None:
+ # If the user has not changed the icon size, then we set the standard
+ # icon size according to the standards of material design version 3.
+ if (
+ self.font_name == "Icons"
+ and self.theme_cls.font_styles["Icon"][1] == self.font_size
+ ):
+ self.font_size = (
+ "18sp"
+ if not self.source and not isinstance(self, MDChipLeadingAvatar)
+ else "24sp"
+ )
+ if self.source and isinstance(self, MDChipLeadingAvatar):
+ self.icon = self.source
+ self._size = [dp(28), dp(28)]
+ self.font_size = "28sp"
+ self.padding_x = "6dp"
+ self._no_ripple_effect = True
+
+
+class LabelTextContainer(MDBoxLayout):
+ """Implements a container for the chip label."""
+
+
+class LeadingIconContainer(MDBoxLayout):
+ """Implements a container for the leading icon."""
+
+
+class TrailingIconContainer(MDBoxLayout):
+ """Implements a container for the trailing icon."""
+
+
+class MDChipLeadingAvatar(BaseChipIcon):
+ """
+ Implements the leading avatar for the chip.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and
+ :class:`~kivymd.uix.behaviors.ScaleBehavior` and
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~kivymd.uix.label.MDIcon`
+ classes documentation.
+ """
+
+
+class MDChipLeadingIcon(BaseChipIcon):
+ """
+ Implements the leading icon for the chip.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and
+ :class:`~kivymd.uix.behaviors.ScaleBehavior` and
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~kivymd.uix.label.MDIcon`
+ classes documentation.
+ """
+
+
+class MDChipTrailingIcon(BaseChipIcon):
+ """
+ Implements the trailing icon for the chip.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and
+ :class:`~kivymd.uix.behaviors.ScaleBehavior` and
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~kivymd.uix.label.MDIcon`
+ classes documentation.
+ """
+
+
+class MDChipText(MDLabel):
+ """
+ Implements the label for the chip.
+
+ For more information, see in the
+ :class:`~kivymd.uix.label.MDLabel` classes documentation.
+ """
+
+
class MDChip(
MDBoxLayout,
- ThemableBehavior,
RectangularRippleBehavior,
ButtonBehavior,
CommonElevationBehavior,
TouchBehavior,
):
- text = StringProperty()
+ """
+ Chip class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` and
+ :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~kivymd.uix.behaviors.CommonElevationBehavior` and
+ :class:`~kivymd.uix.behaviors.TouchBehavior`
+ classes documentation.
+ """
+
+ radius = VariableListProperty([dp(8)], length=4)
+ """
+ Chip radius.
+
+ :attr:`radius` is an :class:`~kivy.properties.VariableListProperty`
+ and defaults to `[dp(8), dp(8), dp(8), dp(8)]`.
+ """
+
+ text = StringProperty(deprecated=True)
"""
Chip text.
+ .. deprecated:: 1.2.0
+
:attr:`text` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
- icon_left = StringProperty()
+ type = OptionProperty(
+ "suggestion", options=["assist", "filter", "input", "suggestion"]
+ )
+ """
+ Type of chip.
+
+ .. versionadded:: 1.2.0
+
+ Available options are: `'assist'`, `'filter'`, `'input'`, `'suggestion'`.
+
+ :attr:`type` is an :class:`~kivy.properties.OptionProperty`
+ and defaults to `'suggestion'`.
+ """
+
+ icon_left = StringProperty(deprecated=True)
"""
Chip left icon.
.. versionadded:: 1.0.0
+ .. deprecated:: 1.2.0
+
:attr:`icon_left` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
- icon_right = StringProperty()
+ icon_right = StringProperty(deprecated=True)
"""
Chip right icon.
.. versionadded:: 1.0.0
+ .. deprecated:: 1.2.0
+
:attr:`icon_right` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
- text_color = ColorProperty(None)
+ text_color = ColorProperty(None, deprecated=True)
"""
- Chip's text color in ``rgba`` format.
+ Chip's text color in (r, g, b, a) or string format.
+
+ .. deprecated:: 1.2.0
:attr:`text_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
- icon_right_color = ColorProperty(None)
+ icon_right_color = ColorProperty(None, deprecated=True)
"""
- Chip's right icon color in ``rgba`` format.
+ Chip's right icon color in (r, g, b, a) or string format.
.. versionadded:: 1.0.0
+ .. deprecated:: 1.2.0
+
:attr:`icon_right_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
- icon_left_color = ColorProperty(None)
+ icon_left_color = ColorProperty(None, deprecated=True)
"""
- Chip's left icon color in ``rgba`` format.
+ Chip's left icon color in (r, g, b, a) or string format.
.. versionadded:: 1.0.0
+ .. deprecated:: 1.2.0
+
:attr:`icon_left_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
icon_check_color = ColorProperty(None)
"""
- Chip's check icon color in ``rgba`` format.
+ Chip's check icon color in (r, g, b, a) or string format.
.. versionadded:: 1.0.0
@@ -411,171 +964,281 @@ class MDChip(
and defaults to `False`.
"""
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
-
- def on_long_touch(self, *args) -> None:
- if self.active:
- return
- self.active = True if not self.active else False
-
- def on_active(self, instance_check, active_value: bool) -> None:
- if active_value:
- self.do_animation_check((0, 0, 0, 0.4), 1)
- else:
- self.do_animation_check((0, 0, 0, 0), 0)
-
- def do_animation_check(self, md_bg_color: list, scale_value: int) -> None:
- Animation(md_bg_color=md_bg_color, t="out_sine", d=0.1).start(
- self.ids.icon_left_box
- )
- Animation(
- scale_value_x=scale_value,
- scale_value_y=scale_value,
- scale_value_z=scale_value,
- t="out_sine",
- d=0.1,
- ).start(self.ids.check_icon)
-
- if not self.icon_left:
- if scale_value:
- self.ids.check_icon.x = -dp(4)
- Animation(size=(dp(24), dp(24)), t="out_sine", d=0.1).start(
- self.ids.relative_box
- )
- else:
- self.ids.check_icon.x = 0
- Animation(size=(0, 0), t="out_sine", d=0.1).start(
- self.ids.relative_box
- )
-
- def on_press(self, *args):
- if self.active:
- self.active = False
-
-
-class MDScalableCheckIcon(MDIcon, ScaleBehavior):
- pos_hint = {"center_y": 0.5}
-
-
-if __name__ == "__main__":
- from kivymd.app import MDApp
- from kivymd.uix.screen import MDScreen
-
- KV = """
-<MyScreen>
+ selected_color = ColorProperty(None)
+ """
+ The background color of the chip in the marked state in (r, g, b, a)
+ or string format.
- MDBoxLayout:
- orientation: "vertical"
- adaptive_size: True
- spacing: "12dp"
- padding: "56dp"
- pos_hint: {"center_x": .5, "center_y": .5}
-
- MDLabel:
- text: "Multiple choose"
- bold: True
- font_style: "H5"
- adaptive_size: True
+ .. versionadded:: 1.2.0
- MDBoxLayout:
- id: chip_box
- adaptive_size: True
- spacing: "8dp"
-
- MyChip:
- text: "Elevator"
- on_press: if self.active: root.removes_marks_all_chips()
+ :attr:`selected_color` is an :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
+ """
- MyChip:
- text: "Washer / Dryer"
- on_press: if self.active: root.removes_marks_all_chips()
+ _current_md_bg_color = ColorProperty(None)
+ # A flag that disallow ripple animation of the chip
+ # at the time of clicking the chip icons.
+ _allow_chip_ripple = BooleanProperty(True)
+ # The flag signals the end of the ripple animation.
+ _anim_complete = BooleanProperty(False)
- MyChip:
- text: "Fireplace"
- on_press: if self.active: root.removes_marks_all_chips()
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
- MDSeparator:
+ def on_long_touch(self, *args) -> None:
+ if self.type == "filter":
+ self.active = not self.active
- MDLabel:
- text: "Only choose"
- bold: True
- font_style: "H5"
- adaptive_size: True
+ def on_type(self, instance, value: str) -> None:
+ """Called when the values of :attr:`type` change."""
- MDBoxLayout:
- id: chip_only_box
- adaptive_size: True
- spacing: "8dp"
+ def adjust_padding(*args):
+ """
+ According to the type of chip, it sets the margins according
+ to the specification of the material design version 3.
+ """
- MyChip:
- text: "Elevator"
- on_active: if self.active: root.removes_marks_all_chips(self, False)
+ self.padding = {
+ "input": (
+ "12dp"
+ if not self.ids.leading_icon_container.children
+ else (
+ "5dp"
+ if not self.ids.leading_icon_container.children[
+ 0
+ ].source
+ else "16dp"
+ ),
+ 0,
+ "4dp",
+ 0,
+ ),
+ "assist": (
+ "16dp"
+ if not self.ids.leading_icon_container.children
+ else "8dp",
+ 0,
+ "16dp"
+ if not self.ids.leading_icon_container.children
+ else "8dp",
+ 0,
+ ),
+ "suggestion": (
+ "16dp"
+ if not self.ids.leading_icon_container.children
+ else "8dp",
+ 0,
+ "16dp",
+ 0,
+ ),
+ "filter": (
+ "16dp"
+ if not self.ids.leading_icon_container.children
+ else (
+ "8dp"
+ if not self.ids.leading_icon_container.children[
+ 0
+ ].source
+ else "4dp"
+ ),
+ 0,
+ "16dp"
+ if not self.ids.trailing_icon_container.children
+ else "8dp",
+ 0,
+ ),
+ }[value]
+
+ Clock.schedule_once(adjust_padding)
- MyChip:
- text: "Washer / Dryer"
- on_active: if self.active: root.removes_marks_all_chips(self, False)
+ def on_active(self, instance_check, active_value: bool) -> None:
+ """Called when the values of :attr:`active` change."""
- MyChip:
- text: "Fireplace"
- on_active: if self.active: root.removes_marks_all_chips(self, False)
+ if active_value:
+ self._current_md_bg_color = self.md_bg_color
+ Clock.schedule_once(self.complete_anim_ripple, 0.5)
-ScreenManager:
+ def complete_anim_ripple(self, *args) -> None:
+ """Called at the end of the ripple animation."""
- MyScreen:
- """
+ if self.active:
+ if not self.ids.leading_icon_container.children:
+ if self.type == "filter":
+ self.add_marked_icon_to_chip()
+ self.set_chip_bg_color(
+ self.selected_color
+ if self.selected_color
+ else self.theme_cls.primary_color
+ )
+ else:
+ if (
+ self.ids.leading_icon_container.children
+ and self.ids.leading_icon_container.children[0].icon == "check"
+ ):
+ if self.type == "filter":
+ self.remove_marked_icon_from_chip()
+ self.set_chip_bg_color(self._current_md_bg_color)
+
+ def remove_marked_icon_from_chip(self) -> None:
+ def remove_marked_icon_from_chip(*args):
+ self.ids.leading_icon_container.clear_widgets()
+
+ if self.ids.leading_icon_container.children:
+ anim = Animation(scale_value_x=0, scale_value_y=0, d=0.2)
+ anim.bind(on_complete=remove_marked_icon_from_chip)
+ anim.start(self.ids.leading_icon_container.children[0])
+ Animation(
+ padding=[dp(16), 0, dp(16), 0],
+ spacing=0,
+ d=0.2,
+ ).start(self)
+
+ def add_marked_icon_to_chip(self) -> None:
+ """Adds and animates a check icon to the chip."""
+
+ icon_check = MDChipLeadingIcon(
+ icon="check",
+ pos_hint={"center_y": 0.5},
+ font_size=dp(18),
+ scale_value_x=0,
+ scale_value_y=0,
+ )
+ icon_check.bind(
+ on_press=self._set_allow_chip_ripple,
+ on_release=self._set_allow_chip_ripple,
+ )
+ self.ids.leading_icon_container.add_widget(icon_check)
+ # Animating the scale of the icon.
+ Animation(scale_value_x=1, scale_value_y=1, d=0.2).start(icon_check)
+ # Animating the padding of the chip.
+ Animation(
+ padding=[dp(18), 0, 0, 0],
+ spacing=dp(18) if self.type == "filter" else 0,
+ d=0.2,
+ ).start(self)
- class MyChip(MDChip):
- icon_check_color = (0, 0, 0, 1)
- text_color = (0, 0, 0, 0.5)
- _no_ripple_effect = True
+ def set_chip_bg_color(self, color: list | str) -> None:
+ """Animates the background color of the chip."""
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.bind(active=self.set_chip_bg_color)
- self.bind(active=self.set_chip_text_color)
+ if color:
+ Animation(md_bg_color=color, d=0.2).start(self)
+ self._anim_complete = not self._anim_complete
- def set_chip_bg_color(self, instance_chip, active_value: int):
- """
- Will be called every time the chip is activated/deactivated.
- Sets the background color of the chip.
- """
+ def on_press(self, *args):
+ if self.active:
+ self.active = False
- self.md_bg_color = (
- (0, 0, 0, 0.4)
- if active_value
- else (
- self.theme_cls.bg_darkest
- if self.theme_cls.theme_style == "Light"
- else (
- self.theme_cls.bg_light
- if not self.disabled
- else self.theme_cls.disabled_hint_text_color
+ def add_widget(self, widget, *args, **kwargs):
+ def add_icon_leading_trailing(container):
+ if len(container.children):
+ type_icon = (
+ "'leading'"
+ if isinstance(
+ widget, (MDChipLeadingIcon, MDChipLeadingAvatar)
)
+ else "'trailing'"
)
- )
-
- def set_chip_text_color(self, instance_chip, active_value: int):
- Animation(
- color=(0, 0, 0, 1) if active_value else (0, 0, 0, 0.5), d=0.2
- ).start(self.ids.label)
+ Logger.warning(
+ f"KivyMD: "
+ f"Do not use more than one {type_icon} icon. "
+ f"This is contrary to the material design rules "
+ f"of version 3"
+ )
+ return
+ if isinstance(widget, MDChipTrailingIcon) and self.type in [
+ "assist",
+ "suggestion",
+ ]:
+ Logger.warning(
+ f"KivyMD: "
+ f"According to the material design standards of version "
+ f"3, do not use the trailing icon for an '{self.type}' "
+ f"type chip."
+ )
+ return
+ if (
+ isinstance(widget, MDChipTrailingIcon)
+ and self.type == "filter"
+ and DEVICE_TYPE == "mobile"
+ ):
+ Logger.warning(
+ "KivyMD: "
+ "According to the material design standards of version 3, "
+ "only on desktop computers and tablets, filter chips can "
+ "contain a finishing icon for directly removing the chip "
+ "or opening the options menu."
+ )
+ return
+ if (
+ isinstance(widget, (MDChipLeadingIcon, MDChipLeadingAvatar))
+ and self.type == "filter"
+ ):
+ Logger.warning(
+ "KivyMD: "
+ "According to the material design standards of version 3, "
+ "it is better not to use a leading icon for a 'filter' "
+ "type chip."
+ )
+ if (
+ isinstance(widget, MDChipLeadingAvatar)
+ and self.type == "suggestion"
+ ):
+ Logger.warning(
+ "KivyMD: "
+ "According to the material design standards of version 3, "
+ "it is better not to use a leading avatar for a "
+ "'suggestion' type chip."
+ )
+ return
- class MyScreen(MDScreen):
- def removes_marks_all_chips(
- self, selected_instance_chip=None, multiple=True
+ widget.bind(
+ on_press=self._set_allow_chip_ripple,
+ on_release=self._set_allow_chip_ripple,
+ )
+ widget.pos_hint = {"center_y": 0.5}
+ self.padding = ("8dp", 0, "8dp", 0)
+ self.spacing = (
+ "8dp"
+ if isinstance(
+ widget,
+ (
+ MDChipLeadingIcon,
+ MDChipLeadingAvatar,
+ MDChipTrailingIcon,
+ ),
+ )
+ else 0
+ )
+ container.add_widget(widget)
+
+ if isinstance(widget, MDChipText):
+ widget.adaptive_size = True
+ widget.pos_hint = {"center_y": 0.5}
+ if self.type == "suggestion":
+ self.padding = ("16dp", 0, "16dp", 0)
+ Clock.schedule_once(
+ lambda x: self.ids.label_container.add_widget(widget)
+ )
+ elif isinstance(widget, (MDChipLeadingIcon, MDChipLeadingAvatar)):
+ Clock.schedule_once(
+ lambda x: add_icon_leading_trailing(
+ self.ids.leading_icon_container
+ )
+ )
+ elif isinstance(widget, MDChipTrailingIcon):
+ Clock.schedule_once(
+ lambda x: add_icon_leading_trailing(
+ self.ids.trailing_icon_container
+ )
+ )
+ elif isinstance(
+ widget,
+ (LabelTextContainer, LeadingIconContainer, TrailingIconContainer),
):
- if multiple:
- for instance_chip in self.ids.chip_box.children:
- if instance_chip.active:
- instance_chip.active = False
- else:
- for instance_chip in self.ids.chip_only_box.children:
- if instance_chip != selected_instance_chip:
- instance_chip.active = False
+ return super().add_widget(widget)
- class Test(MDApp):
- def build(self):
- return Builder.load_string(KV)
-
- Test().run()
+ def _set_allow_chip_ripple(
+ self, instance: MDChipLeadingIcon | MDChipTrailingIcon
+ ) -> None:
+ self._allow_chip_ripple = not self._allow_chip_ripple

diff --git a/sbapp/kivymd/uix/circularlayout.py b/sbapp/kivymd/uix/circularlayout.py
index 9f02e853..cca5d21d 100644
--- a/sbapp/kivymd/uix/circularlayout.py
+++ b/sbapp/kivymd/uix/circularlayout.py
@@ -53,7 +53,6 @@ from kivymd.uix.floatlayout import MDFloatLayout
class MDCircularLayout(MDFloatLayout):
-
degree_spacing = NumericProperty(30)
"""
The space between children in degree.

diff --git a/sbapp/kivymd/uix/datatables/datatables.kv b/sbapp/kivymd/uix/datatables/datatables.kv
index a6a72201..b5d81a7a 100644
--- a/sbapp/kivymd/uix/datatables/datatables.kv
+++ b/sbapp/kivymd/uix/datatables/datatables.kv
@@ -231,4 +231,11 @@
id: container
orientation: "vertical"
elevation: root.elevation
+ shadow_radius: root.shadow_radius
+ shadow_softness: root.shadow_softness
+ shadow_offset: root.shadow_offset
+ shadow_color: root.shadow_color
+ shadow_color: root.shadow_color
+ shadow_softness_size: root.shadow_softness_size
padding: "24dp", "24dp", "8dp", "8dp"
+ md_bg_color: app.theme_cls.bg_normal

diff --git a/sbapp/kivymd/uix/datatables/datatables.py b/sbapp/kivymd/uix/datatables/datatables.py
index 16262838..44748a55 100644
--- a/sbapp/kivymd/uix/datatables/datatables.py
+++ b/sbapp/kivymd/uix/datatables/datatables.py
@@ -37,6 +37,7 @@ from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
BooleanProperty,
+ BoundedNumericProperty,
ColorProperty,
DictProperty,
ListProperty,
@@ -44,6 +45,7 @@ from kivy.properties import (
ObjectProperty,
OptionProperty,
StringProperty,
+ VariableListProperty,
)
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.behaviors import ButtonBehavior, FocusBehavior
@@ -56,6 +58,11 @@ from kivy.uix.scrollview import ScrollView
from kivymd import uix_path
from kivymd.effects.stiffscroll import StiffScrollEffect
+from kivymd.material_resources import (
+ DATA_TABLE_ELEVATION,
+ DATA_TABLE_OFFSET,
+ DATA_TABLE_SOFTNESS,
+)
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import HoverBehavior
from kivymd.uix.boxlayout import MDBoxLayout
@@ -758,7 +765,7 @@ class TableData(RecycleView):
# instance_pagination.ids.button_forward.disabled = True
-class TablePagination(ThemableBehavior, MDBoxLayout):
+class TablePagination(MDBoxLayout):
"""Pagination Container."""
table_data = ObjectProperty()
@@ -772,8 +779,11 @@ class TablePagination(ThemableBehavior, MDBoxLayout):
class MDDataTable(ThemableBehavior, AnchorLayout):
"""
- See :class:`~kivy.uix.anchorlayout.AnchorLayout` class documentation for
- more information.
+ Datatable class.
+
+ For more information, see in the
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivy.uix.anchorlayout.AnchorLayout` classes documentation.
:Events:
:attr:`on_row_press`
@@ -1297,14 +1307,70 @@ class MDDataTable(ThemableBehavior, AnchorLayout):
and defaults to `False`.
"""
- elevation = NumericProperty(4)
+ elevation = NumericProperty(DATA_TABLE_ELEVATION)
"""
- Table elevation.
+ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.elevation`
+ attribute.
:attr:`elevation` is an :class:`~kivy.properties.NumericProperty`
and defaults to `4`.
"""
+ shadow_radius = VariableListProperty([6], length=4)
+ """
+ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_radius`
+ attribute.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`shadow_radius` is an :class:`~kivy.properties.VariableListProperty`
+ and defaults to `[6]`.
+ """
+
+ shadow_softness = NumericProperty(DATA_TABLE_SOFTNESS)
+ """
+ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_softness`
+ attribute.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`shadow_softness` is an :class:`~kivy.properties.NumericProperty`
+ and defaults to `12`.
+ """
+
+ shadow_softness_size = BoundedNumericProperty(2, min=2)
+ """
+ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_softness_size`
+ attribute.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`shadow_softness_size` is an :class:`~kivy.properties.BoundedNumericProperty`
+ and defaults to `2`.
+ """
+
+ shadow_offset = ListProperty(DATA_TABLE_OFFSET)
+ """
+ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_offset`
+ attribute.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`shadow_offset` is an :class:`~kivy.properties.ListProperty`
+ and defaults to `(0, 2)`.
+ """
+
+ shadow_color = ColorProperty([0, 0, 0, 0.6])
+ """
+ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_color`
+ attribute.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`shadow_color` is an :class:`~kivy.properties.ColorProperty`
+ and defaults to `[0, 0, 0, 0.6]`.
+ """
+
rows_num = NumericProperty(5)
"""
The number of rows displayed on one page of the table.
@@ -1626,6 +1692,77 @@ class MDDataTable(ThemableBehavior, AnchorLayout):
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-add-remove-row.gif
:align: center
+ Deleting checked rows
+ ---------------------
+
+ .. code-block:: python
+
+ from kivy.metrics import dp
+ from kivy.lang import Builder
+ from kivy.clock import Clock
+
+ from kivymd.app import MDApp
+ from kivymd.uix.datatables import MDDataTable
+ from kivymd.uix.screen import MDScreen
+
+ KV = '''
+ MDBoxLayout:
+ orientation: "vertical"
+ padding: "56dp"
+ spacing: "24dp"
+
+ MDData:
+ id: table_screen
+
+ MDRaisedButton:
+ text: "DELETE CHECKED ROWS"
+ on_release: table_screen.delete_checked_rows()
+ '''
+
+
+ class MDData(MDScreen):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.data = [
+ ["1", "Asep Sudrajat", "Male", "Soccer"],
+ ["2", "Egy", "Male", "Soccer"],
+ ["3", "Tanos", "Demon", "Soccer"],
+ ]
+ self.data_tables = MDDataTable(
+ use_pagination=True,
+ check=True,
+ column_data=[
+ ("No", dp(30)),
+ ("No Urut.", dp(30)),
+ ("Alamat Pengirim", dp(30)),
+ ("No Surat", dp(60)),
+ ]
+ )
+ self.data_tables.row_data = self.data
+ self.add_widget(self.data_tables)
+
+ def delete_checked_rows(self):
+ def deselect_rows(*args):
+ self.data_tables.table_data.select_all("normal")
+
+ for data in self.data_tables.get_row_checks():
+ self.data_tables.remove_row(data)
+
+ Clock.schedule_once(deselect_rows)
+
+
+ class MyApp(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
+ return Builder.load_string(KV)
+
+
+ MyApp().run()
+
+ .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-deleting-checked-rows.gif
+ :align: center
+
.. versionadded:: 1.0.0
"""
@@ -1760,7 +1897,6 @@ class MDDataTable(ThemableBehavior, AnchorLayout):
class CellRow(
- ThemableBehavior,
RecycleDataViewBehavior,
HoverBehavior,
ButtonBehavior,

diff --git a/sbapp/kivymd/uix/dialog/dialog.kv b/sbapp/kivymd/uix/dialog/dialog.kv
index 2e95b9ca..96e01a37 100644
--- a/sbapp/kivymd/uix/dialog/dialog.kv
+++ b/sbapp/kivymd/uix/dialog/dialog.kv
@@ -32,7 +32,7 @@
orientation: "vertical"
size_hint_y: None
height: self.minimum_height
- padding: "24dp", "24dp", "16dp", "8dp"
+ padding: "24dp", "24dp", "8dp", "8dp"
radius: root.radius
md_bg_color:
root.theme_cls.bg_dark \

diff --git a/sbapp/kivymd/uix/dialog/dialog.py b/sbapp/kivymd/uix/dialog/dialog.py
index f254f308..9f2db53a 100755
--- a/sbapp/kivymd/uix/dialog/dialog.py
+++ b/sbapp/kivymd/uix/dialog/dialog.py
@@ -89,7 +89,7 @@ from kivy.uix.modalview import ModalView
from kivymd import uix_path
from kivymd.material_resources import DEVICE_TYPE
from kivymd.theming import ThemableBehavior
-from kivymd.uix.behaviors import CommonElevationBehavior
+from kivymd.uix.behaviors import CommonElevationBehavior, MotionDialogBehavior
from kivymd.uix.button import BaseButton
from kivymd.uix.card import MDSeparator
from kivymd.uix.list import BaseListItem
@@ -100,7 +100,9 @@ with open(
Builder.load_string(kv_file.read())
-class BaseDialog(ThemableBehavior, ModalView, CommonElevationBehavior):
+class BaseDialog(
+ ThemableBehavior, MotionDialogBehavior, ModalView, CommonElevationBehavior
+):
elevation = NumericProperty(3)
"""
See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.elevation`
@@ -159,6 +161,16 @@ class BaseDialog(ThemableBehavior, ModalView, CommonElevationBehavior):
class MDDialog(BaseDialog):
+ """
+ Dialog class.
+
+ For more information, see in the
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivy.uix.modalview.ModalView` and
+ :class:`~kivymd.uix.behaviors.CommonElevationBehavior`
+ classes documentation.
+ """
+
title = StringProperty()
"""
Title dialog.
@@ -286,22 +298,22 @@ class MDDialog(BaseDialog):
class Example(MDApp):
dialog = None
- def build(self):
- self.theme_cls.theme_style = "Dark"
- self.theme_cls.primary_palette = "Orange"
- return Builder.load_string(KV)
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
+ return Builder.load_string(KV)
- def show_simple_dialog(self):
- if not self.dialog:
- self.dialog = MDDialog(
- title="Set backup account",
- type="simple",
- items=[
- Item(text="user01@gmail.com", source="kivymd/images/logo/kivymd-icon-128.png"),
- Item(text="user02@gmail.com", source="data/logo/kivy-icon-128.png"),
- ],
- )
- self.dialog.open()
+ def show_simple_dialog(self):
+ if not self.dialog:
+ self.dialog = MDDialog(
+ title="Set backup account",
+ type="simple",
+ items=[
+ Item(text="user01@gmail.com", source="kivymd/images/logo/kivymd-icon-128.png"),
+ Item(text="user02@gmail.com", source="data/logo/kivy-icon-128.png"),
+ ],
+ )
+ self.dialog.open()
Example().run()
@@ -422,7 +434,8 @@ class MDDialog(BaseDialog):
content_cls = ObjectProperty()
"""
- Custom content class.
+ Custom content class. This attribute is only available when :attr:`type` is
+ set to `'custom'`.
.. tabs::
@@ -637,6 +650,7 @@ class MDDialog(BaseDialog):
def on_open(self) -> None:
# TODO: Add scrolling text.
self.height = self.ids.container.height
+ super().on_open()
def get_normal_height(self) -> float:
return (

diff --git a/sbapp/kivymd/uix/dropdownitem/dropdownitem.py b/sbapp/kivymd/uix/dropdownitem/dropdownitem.py
index 3967c5cc..951357d7 100644
--- a/sbapp/kivymd/uix/dropdownitem/dropdownitem.py
+++ b/sbapp/kivymd/uix/dropdownitem/dropdownitem.py
@@ -68,6 +68,17 @@ class _Triangle(Widget):
class MDDropDownItem(
DeclarativeBehavior, ThemableBehavior, ButtonBehavior, BoxLayout
):
+ """
+ Dropdown item class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~kivy.uix.boxlayout.BoxLayout`
+ classes documentation.
+ """
+
text = StringProperty()
"""
Text item.

diff --git a/sbapp/kivymd/uix/expansionpanel/expansionpanel.py b/sbapp/kivymd/uix/expansionpanel/expansionpanel.py
index 2bbd8eee..d0fe33db 100755
--- a/sbapp/kivymd/uix/expansionpanel/expansionpanel.py
+++ b/sbapp/kivymd/uix/expansionpanel/expansionpanel.py
@@ -185,21 +185,39 @@ class MDExpansionChevronRight(IRightBodyTouch, MDIconButton):
class MDExpansionPanelOneLine(OneLineAvatarIconListItem):
- """Single line panel."""
+ """
+ Single line panel.
+
+ For more information, see in the
+ :class:`~kivymd.uix.list.OneLineAvatarIconListItem` class documentation.
+ """
class MDExpansionPanelTwoLine(TwoLineAvatarIconListItem):
- """Two-line panel."""
+ """
+ Two-line panel.
+
+ For more information, see in the
+ :class:`~kivymd.uix.list.TwoLineAvatarIconListItem` class documentation.
+ """
class MDExpansionPanelThreeLine(ThreeLineAvatarIconListItem):
- """Three-line panel."""
+ """
+ Three-line panel.
+
+ For more information, see in the
+ :class:`~kivymd.uix.list.ThreeLineAvatarIconListItem` class documentation.
+ """
class MDExpansionPanelLabel(TwoLineListItem):
"""
Label panel.
+ For more information, see in the
+ :class:`~kivymd.uix.list.TwoLineListItem` class documentation.
+
..warning:: This class is created for use in the
:class:`~kivymd.uix.stepper.MDStepperVertical` and
:class:`~kivymd.uix.stepper.MDStepper` classes, and has not
@@ -217,6 +235,11 @@ class MDExpansionPanelLabel(TwoLineListItem):
class MDExpansionPanel(RelativeLayout):
"""
+ Expansion panel class.
+
+ For more information, see in the
+ :class:`~kivy.uix.relativelayout.RelativeLayout` classes documentation.
+
:Events:
:attr:`on_open`
Called when a panel is opened.

diff --git a/sbapp/kivymd/uix/filemanager/filemanager.kv b/sbapp/kivymd/uix/filemanager/filemanager.kv
index ad5704f4..29352d7c 100644
--- a/sbapp/kivymd/uix/filemanager/filemanager.kv
+++ b/sbapp/kivymd/uix/filemanager/filemanager.kv
@@ -1,4 +1,6 @@
#:import os os
+#:import FILE_MANAGER_TOP_APP_BAR_ELEVATION kivymd.material_resources.FILE_MANAGER_TOP_APP_BAR_ELEVATION
+
<BodyManager>
icon: "folder"
@@ -74,7 +76,7 @@
title: root.current_path
right_action_items: [["close-box", lambda x: root.exit_manager(1)]]
left_action_items: [["chevron-left", lambda x: root.back()]]
- elevation: 3
+ elevation: FILE_MANAGER_TOP_APP_BAR_ELEVATION
md_bg_color:
app.theme_cls.primary_color \
if not root.background_color_toolbar else \

diff --git a/sbapp/kivymd/uix/filemanager/filemanager.py b/sbapp/kivymd/uix/filemanager/filemanager.py
index 1b7727ef..4073764c 100755
--- a/sbapp/kivymd/uix/filemanager/filemanager.py
+++ b/sbapp/kivymd/uix/filemanager/filemanager.py
@@ -158,7 +158,6 @@ from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.modalview import ModalView
from kivymd import images_path, uix_path
-from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import CircularRippleBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import MDFloatingActionButton
@@ -197,7 +196,7 @@ class ModifiedOneLineIconListItem(BaseListItem):
self.height = dp(48)
-class MDFileManager(MDRelativeLayout, ThemableBehavior):
+class MDFileManager(MDRelativeLayout):
"""
Implements a modal dialog with a file manager.
@@ -248,7 +247,8 @@ class MDFileManager(MDRelativeLayout, ThemableBehavior):
background_color_selection_button = ColorProperty(None)
"""
- Background color of the current directory/path selection button.
+ Background color in (r, g, b, a) or string format of the current
+ directory/path selection button.
.. versionadded:: 1.1.0
@@ -268,7 +268,7 @@ class MDFileManager(MDRelativeLayout, ThemableBehavior):
background_color_toolbar = ColorProperty(None)
"""
- Background color of the file manager toolbar.
+ Background color in (r, g, b, a) or string format of the file manager toolbar.
.. versionadded:: 1.1.0
@@ -307,7 +307,8 @@ class MDFileManager(MDRelativeLayout, ThemableBehavior):
icon_color = ColorProperty(None)
"""
- Color of the folder icon when the :attr:`preview` property is set to False.
+ Color in (r, g, b, a) or string format of the folder icon when the
+ :attr:`preview` property is set to False.
.. versionadded:: 1.1.0

diff --git a/sbapp/kivymd/uix/fitimage/fitimage.py b/sbapp/kivymd/uix/fitimage/fitimage.py
index 35ded4d9..246bb5a2 100644
--- a/sbapp/kivymd/uix/fitimage/fitimage.py
+++ b/sbapp/kivymd/uix/fitimage/fitimage.py
@@ -137,6 +137,14 @@ from kivymd.uix.boxlayout import MDBoxLayout
class FitImage(MDBoxLayout, StencilBehavior):
+ """
+ Fit image class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.boxlayout.MDLayout` and
+ :class:`~kivymd.uix.behaviors.StencilBehavior` classes documentation.
+ """
+
source = ObjectProperty()
"""
Filename/source of your image.

diff --git a/sbapp/kivymd/uix/floatlayout.py b/sbapp/kivymd/uix/floatlayout.py
index 3f99981b..b64ddcfc 100644
--- a/sbapp/kivymd/uix/floatlayout.py
+++ b/sbapp/kivymd/uix/floatlayout.py
@@ -35,11 +35,14 @@ MDFloatLayout
from kivy.uix.floatlayout import FloatLayout
+from kivymd.theming import ThemableBehavior
from kivymd.uix import MDAdaptiveWidget
from kivymd.uix.behaviors import DeclarativeBehavior
-class MDFloatLayout(DeclarativeBehavior, FloatLayout, MDAdaptiveWidget):
+class MDFloatLayout(
+ DeclarativeBehavior, ThemableBehavior, FloatLayout, MDAdaptiveWidget
+):
"""
Float layout class. For more information, see in the
:class:`~kivy.uix.floatlayout.FloatLayout` class documentation.

diff --git a/sbapp/kivymd/uix/gridlayout.py b/sbapp/kivymd/uix/gridlayout.py
index 84d883cf..96bfd79c 100644
--- a/sbapp/kivymd/uix/gridlayout.py
+++ b/sbapp/kivymd/uix/gridlayout.py
@@ -85,11 +85,14 @@ Equivalent
from kivy.uix.gridlayout import GridLayout
+from kivymd.theming import ThemableBehavior
from kivymd.uix import MDAdaptiveWidget
from kivymd.uix.behaviors import DeclarativeBehavior
-class MDGridLayout(DeclarativeBehavior, GridLayout, MDAdaptiveWidget):
+class MDGridLayout(
+ DeclarativeBehavior, ThemableBehavior, GridLayout, MDAdaptiveWidget
+):
"""
Grid layout class. For more information, see in the
:class:`~kivy.uix.gridlayout.GridLayout` class documentation.

diff --git a/sbapp/kivymd/uix/imagelist/imagelist.kv b/sbapp/kivymd/uix/imagelist/imagelist.kv
index 8cb474c7..fe87f769 100644
--- a/sbapp/kivymd/uix/imagelist/imagelist.kv
+++ b/sbapp/kivymd/uix/imagelist/imagelist.kv
@@ -13,6 +13,7 @@
(0, 0)
on_release: root.dispatch("on_release")
on_press: root.dispatch("on_press")
+ _no_ripple_effect: root._no_ripple_effect
SmartTileOverlayBox:
id: box

diff --git a/sbapp/kivymd/uix/imagelist/imagelist.py b/sbapp/kivymd/uix/imagelist/imagelist.py
old mode 100755
new mode 100644
index 970ed2e5..ba84b14d
--- a/sbapp/kivymd/uix/imagelist/imagelist.py
+++ b/sbapp/kivymd/uix/imagelist/imagelist.py
@@ -71,6 +71,7 @@ __all__ = [
import os
+from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import (
BooleanProperty,
@@ -82,7 +83,6 @@ from kivy.properties import (
from kivy.uix.behaviors import ButtonBehavior
from kivymd import uix_path
-from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import RectangularRippleBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.fitimage import FitImage
@@ -103,10 +103,13 @@ class SmartTileOverlayBox(MDBoxLayout):
"""Implements a container for custom widgets to be added to the tile."""
-class MDSmartTile(MDRelativeLayout, ThemableBehavior):
+class MDSmartTile(MDRelativeLayout):
"""
A tile for more complex needs.
+ For more information, see in the
+ :class:`~kivymd.uix.relativelayout.MDRelativeLayout` class documentation.
+
Includes an image, a container to place overlays and a box that can act
as a header or a footer, as described in the Material Design specs.
@@ -139,7 +142,8 @@ class MDSmartTile(MDRelativeLayout, ThemableBehavior):
box_color = ColorProperty((0, 0, 0, 0.5))
"""
- Sets the color and opacity for the information box.
+ Sets the color in (r, g, b, a) or string format and opacity for the
+ information box.
.. code-block:: kv
@@ -249,6 +253,8 @@ class MDSmartTile(MDRelativeLayout, ThemableBehavior):
and defaults to `False`.
"""
+ _no_ripple_effect = BooleanProperty(False)
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.register_event_type("on_release")
@@ -270,4 +276,4 @@ class MDSmartTile(MDRelativeLayout, ThemableBehavior):
if isinstance(widget, MDLabel):
widget.shorten = True
widget.shorten_from = "right"
- self.ids.box.add_widget(widget)
+ Clock.schedule_once(lambda x: self.ids.box.add_widget(widget))

diff --git a/sbapp/kivymd/uix/label/label.kv b/sbapp/kivymd/uix/label/label.kv
index 71cac919..7fba3482 100644
--- a/sbapp/kivymd/uix/label/label.kv
+++ b/sbapp/kivymd/uix/label/label.kv
@@ -3,12 +3,10 @@
<MDLabel>
disabled_color: self.theme_cls.disabled_hint_text_color
- # FIXME: Overriding the values of this property greatly affects application
- # performance. Especially when the application window is resized and a
- # custom font is used. Performance is especially slow when you are using
- # `PIL` as your text processing provider - os.environ ['KIVY_TEXT'] = 'pil'.
- # Priority - CRITICAL.
- text_size: self.width, None
+ text_size:
+ (self.width if not self.adaptive_width else None) \
+ if not self.adaptive_size else None, \
+ None
<MDIcon>:
@@ -16,6 +14,7 @@
Color:
rgba: (1, 1, 1, 1) if self.source else (0, 0, 0, 0)
Rectangle:
+ group: "rectangle"
source: self.source if self.source else None
pos:
self.pos \
@@ -32,6 +31,7 @@
# Badge icon.
MDLabel:
+ id: badge
font_style: "Icon"
adaptive_size: True
opposite_icon_color: True
@@ -62,6 +62,7 @@
if root.badge_icon else \
(0, 0, 0, 0)
RoundedRectangle:
+ group: "badge"
radius: [self.width / 2,]
pos: self.pos
size: self.size

diff --git a/sbapp/kivymd/uix/label/label.py b/sbapp/kivymd/uix/label/label.py
index fc34ba4f..73bd46b6 100755
--- a/sbapp/kivymd/uix/label/label.py
+++ b/sbapp/kivymd/uix/label/label.py
@@ -18,32 +18,58 @@ Class :class:`MDLabel` inherited from the :class:`~kivy.uix.label.Label` class
but for :class:`MDLabel` the ``text_size`` parameter is ``(self.width, None)``
and default is positioned on the left:
-.. code-block:: python
+.. tabs::
- from kivy.lang import Builder
+ .. tab:: Declarative KV style
- from kivymd.app import MDApp
+ .. code-block:: python
- KV = '''
- MDScreen:
+ from kivy.lang import Builder
- MDBoxLayout:
- orientation: "vertical"
+ from kivymd.app import MDApp
- MDTopAppBar:
- title: "MDLabel"
+ KV = '''
+ MDScreen:
- MDLabel:
- text: "MDLabel"
- '''
+ MDLabel:
+ text: "MDLabel"
+ '''
- class Test(MDApp):
- def build(self):
- return Builder.load_string(KV)
+ class Test(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
+ return Builder.load_string(KV)
- Test().run()
+ Test().run()
+
+ .. tab:: Declarative Python style
+
+ .. code-block:: python
+
+ from kivy.lang import Builder
+
+ from kivymd.app import MDApp
+ from kivymd.uix.screen import MDScreen
+ from kivymd.uix.label import MDLabel
+
+
+ class Test(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
+ return (
+ MDScreen(
+ MDLabel(
+ text="MDLabel"
+ )
+ )
+ )
+
+
+ Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-to-left.png
:align: center
@@ -74,20 +100,16 @@ and default is positioned on the left:
from kivymd.uix.label import MDLabel
KV = '''
- MDScreen:
-
- MDBoxLayout:
- id: box
- orientation: "vertical"
-
- MDTopAppBar:
- title: "MDLabel"
+ MDBoxLayout:
+ orientation: "vertical"
'''
class Test(MDApp):
def build(self):
+ self.theme_cls.theme_style = "Dark"
screen = Builder.load_string(KV)
+
# Names of standard color themes.
for name_theme in [
"Primary",
@@ -96,7 +118,7 @@ and default is positioned on the left:
"Error",
"ContrastParentBackground",
]:
- screen.ids.box.add_widget(
+ screen.add_widget(
MDLabel(
text=name_theme,
halign="center",
@@ -121,7 +143,7 @@ in the ``text_color`` parameter:
text: "Custom color"
halign: "center"
theme_text_color: "Custom"
- text_color: 0, 0, 1, 1
+ text_color: "blue"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-custom-color.png
:align: center
@@ -138,26 +160,20 @@ parameter:
from kivymd.uix.label import MDLabel
from kivymd.font_definitions import theme_font_styles
-
KV = '''
- MDScreen:
+ MDScrollView:
- MDBoxLayout:
- orientation: "vertical"
-
- MDTopAppBar:
- title: "MDLabel"
-
- ScrollView:
-
- MDList:
- id: box
+ MDList:
+ id: box
+ spacing: "8dp"
'''
class Test(MDApp):
def build(self):
+ self.theme_cls.theme_style = "Dark"
screen = Builder.load_string(KV)
+
# Names of standard font styles.
for name_style in theme_font_styles[:-1]:
screen.ids.box.add_widget(
@@ -165,6 +181,7 @@ parameter:
text=f"{name_style} style",
halign="center",
font_style=name_style,
+ adaptive_height=True,
)
)
return screen
@@ -172,7 +189,273 @@ parameter:
Test().run()
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-font-style.gif
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-font-style.png
+ :align: center
+
+Highlighting and copying labels
+===============================
+
+You can highlight labels by double tap on the label:
+----------------------------------------------------
+
+.. tabs::
+
+ .. tab:: Declarative KV style
+
+ .. code-block:: python
+
+ from kivy.lang.builder import Builder
+
+ from kivymd.app import MDApp
+
+ KV = '''
+ MDScreen:
+
+ MDLabel:
+ adaptive_size: True
+ pos_hint: {"center_x": .5, "center_y": .5}
+ text: "MDLabel"
+ allow_selection: True
+ padding: "4dp", "4dp"
+ '''
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
+ return Builder.load_string(KV)
+
+
+ Example().run()
+
+ .. tab:: Declarative Python style
+
+ .. code-block:: python
+
+ from kivy.lang.builder import Builder
+
+ from kivymd.app import MDApp
+ from kivymd.uix.label import MDLabel
+ from kivymd.uix.screen import MDScreen
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
+ return (
+ MDScreen(
+ MDLabel(
+ adaptive_size=True,
+ pos_hint={"center_x": .5, "center_y": .5},
+ text="MDLabel",
+ allow_selection=True,
+ padding=("4dp", "4dp"),
+ )
+ )
+ )
+
+
+ Example().run()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-allow-selection.gif
+ :align: center
+
+You can copy the label text by double clicking on it:
+-----------------------------------------------------
+
+.. tabs::
+
+ .. tab:: Declarative KV style
+
+ .. code-block:: python
+
+ from kivy.lang.builder import Builder
+
+ from kivymd.app import MDApp
+
+ KV = '''
+ MDScreen:
+
+ MDLabel:
+ adaptive_size: True
+ pos_hint: {"center_x": .5, "center_y": .5}
+ text: "MDLabel"
+ padding: "4dp", "4dp"
+ allow_selection: True
+ allow_copy: True
+ on_copy: print("The text is copied to the clipboard")
+ '''
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
+ return Builder.load_string(KV)
+
+
+ Example().run()
+
+ .. tab:: Declarative Python style
+
+ .. code-block:: python
+
+ from kivy.lang.builder import Builder
+
+ from kivymd.app import MDApp
+ from kivymd.uix.label import MDLabel
+ from kivymd.uix.screen import MDScreen
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
+ return (
+ MDScreen(
+ MDLabel(
+ id="label",
+ adaptive_size=True,
+ pos_hint={"center_x": .5, "center_y": .5},
+ text="MDLabel",
+ allow_selection=True,
+ allow_copy=True,
+ padding=("4dp", "4dp"),
+ )
+ )
+ )
+
+ def on_start(self):
+ self.root.ids.label.bind(on_copy=self.on_copy)
+
+ def on_copy(self, instance_label: MDLabel):
+ print("The text is copied to the clipboard")
+
+
+ Example().run()
+
+Example of copying/cutting labels using the context menu
+--------------------------------------------------------
+
+.. code-block:: python
+
+ from kivy.core.clipboard import Clipboard
+ from kivy.lang.builder import Builder
+ from kivy.metrics import dp
+
+ from kivymd.app import MDApp
+ from kivymd.uix.label import MDLabel
+ from kivymd.uix.menu import MDDropdownMenu
+ from kivymd.toast import toast
+
+ KV = '''
+ MDBoxLayout:
+ orientation: "vertical"
+ spacing: "12dp"
+ padding: "24dp"
+
+ MDScrollView:
+
+ MDBoxLayout:
+ id: box
+ orientation: "vertical"
+ padding: "24dp"
+ spacing: "12dp"
+ adaptive_height: True
+
+ MDTextField:
+ max_height: "200dp"
+ mode: "fill"
+ multiline: True
+
+ MDWidget:
+ '''
+
+ data = [
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
+ "Sed blandit libero volutpat sed cras ornare arcu. Nisl vel pretium "
+ "lectus quam id leo in. Tincidunt arcu non sodales neque sodales ut etiam.",
+ "Elit scelerisque mauris pellentesque pulvinar pellentesque habitant. "
+ "Nisl rhoncus mattis rhoncus urna neque. Orci nulla pellentesque "
+ "dignissim enim. Ac auctor augue mauris augue neque gravida in fermentum. "
+ "Lacus suspendisse faucibus interdum posuere."
+
+ ]
+
+
+ class CopyLabel(MDLabel):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.allow_selection = True
+ self.adaptive_height = True
+ self.theme_text_color = "Custom"
+ self.text_color = self.theme_cls.text_color
+
+
+ class Example(MDApp):
+ context_menu = None
+
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
+ return Builder.load_string(KV)
+
+ def on_start(self):
+ for text in data:
+ copy_label = CopyLabel(text=text)
+ copy_label.bind(
+ on_selection=self.open_context_menu,
+ on_cancel_selection=self.restore_text_color,
+ )
+ self.root.ids.box.add_widget(copy_label)
+
+ def click_item_context_menu(
+ self, type_click: str, instance_label: CopyLabel
+ ) -> None:
+ Clipboard.copy(instance_label.text)
+
+ if type_click == "copy":
+ toast("Copied")
+ elif type_click == "cut":
+ self.root.ids.box.remove_widget(instance_label)
+ toast("Cut")
+ if self.context_menu:
+ self.context_menu.dismiss()
+
+ def restore_text_color(self, instance_label: CopyLabel) -> None:
+ instance_label.text_color = self.theme_cls.text_color
+
+ def open_context_menu(self, instance_label: CopyLabel) -> None:
+ instance_label.text_color = "black"
+ menu_items = [
+ {
+ "text": "Copy text",
+ "viewclass": "OneLineListItem",
+ "height": dp(48),
+ "on_release": lambda: self.click_item_context_menu(
+ "copy", instance_label
+ ),
+ },
+ {
+ "text": "Cut text",
+ "viewclass": "OneLineListItem",
+ "height": dp(48),
+ "on_release": lambda: self.click_item_context_menu(
+ "cut", instance_label
+ ),
+ },
+ ]
+ self.context_menu = MDDropdownMenu(
+ caller=instance_label, items=menu_items, width_mult=3
+ )
+ self.context_menu.open()
+
+
+ Example().run()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/copying-cutting-labels-using-context-menu.gif
:align: center
.. MDIcon:
@@ -217,6 +500,8 @@ MDIcon with badge icon
:align: center
"""
+from __future__ import annotations
+
__all__ = ("MDLabel", "MDIcon")
import os
@@ -224,6 +509,8 @@ from typing import Union
from kivy.animation import Animation
from kivy.clock import Clock
+from kivy.core.clipboard import Clipboard
+from kivy.core.window import Window
from kivy.graphics import Color, Rectangle
from kivy.lang import Builder
from kivy.metrics import sp
@@ -243,7 +530,7 @@ from kivymd import uix_path
from kivymd.theming import ThemableBehavior
from kivymd.theming_dynamic_text import get_contrast_text_color
from kivymd.uix import MDAdaptiveWidget
-from kivymd.uix.behaviors import DeclarativeBehavior
+from kivymd.uix.behaviors import DeclarativeBehavior, TouchBehavior
from kivymd.uix.floatlayout import MDFloatLayout
__MDLabel_colors__ = {
@@ -264,7 +551,36 @@ with open(
Builder.load_string(kv_file.read())
-class MDLabel(DeclarativeBehavior, ThemableBehavior, Label, MDAdaptiveWidget):
+class MDLabel(
+ DeclarativeBehavior,
+ ThemableBehavior,
+ Label,
+ MDAdaptiveWidget,
+ TouchBehavior,
+):
+ """
+ Label class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivy.uix.label.Label` and
+ :class:`~kivymd.uix.MDAdaptiveWidget` and
+ :class:`~kivymd.uix.behaviors.TouchBehavior`
+ classes documentation.
+
+ :Events:
+ `on_ref_press`
+ Called when the user clicks on a word referenced with a
+ ``[ref]`` tag in a text markup.
+ `on_copy`
+ Called when double-tapping on the label.
+ `on_selection`
+ Called when double-tapping on the label.
+ `on_cancel_selection`
+ Called when the highlighting is removed from the label text.
+ """
+
font_style = StringProperty("Body1")
"""
Label font style.
@@ -316,29 +632,84 @@ class MDLabel(DeclarativeBehavior, ThemableBehavior, Label, MDAdaptiveWidget):
text_color = ColorProperty(None)
"""
- Label text color in (r, g, b, a) format.
+ Label text color in (r, g, b, a) or string format.
:attr:`text_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
+ allow_copy = BooleanProperty(False)
+ """
+ Allows you to copy text to the clipboard by double-clicking on the label.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`allow_copy` is an :class:`~kivy.properties.BooleanProperty`
+ and defaults to `False`.
+ """
+
+ allow_selection = BooleanProperty(False)
+ """
+ Allows to highlight text by double-clicking on the label.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`allow_selection` is an :class:`~kivy.properties.BooleanProperty`
+ and defaults to `False`.
+ """
+
+ color_selection = ColorProperty(None)
+ """
+ The color in (r, g, b, a) or string format of the text selection when the
+ value of the :attr:`allow_selection` attribute is True.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`color_selection` is an :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
+ """
+
+ color_deselection = ColorProperty(None)
+ """
+ The color in (r, g, b, a) or string format of the text deselection when the
+ value of the :attr:`allow_selection` attribute is True.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`color_deselection` is an :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
+ """
+
+ is_selected = BooleanProperty(False)
+ """
+ Is the label text highlighted.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`is_selected` is an :class:`~kivy.properties.BooleanProperty`
+ and defaults to `False`.
+ """
+
_text_color_str = StringProperty()
parent_background = ColorProperty(None)
can_capitalize = BooleanProperty(True)
canvas_bg = ObjectProperty()
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
self.bind(
font_style=self.update_font_style,
can_capitalize=self.update_font_style,
)
+ self.theme_cls.bind(theme_style=self._do_update_theme_color)
+ self.register_event_type("on_copy")
+ self.register_event_type("on_selection")
+ self.register_event_type("on_cancel_selection")
self.on_theme_text_color(None, self.theme_text_color)
self.update_font_style(None, "")
self.on_opposite_colors(None, self.opposite_colors)
Clock.schedule_once(self.check_font_styles)
- self.theme_cls.bind(theme_style=self._do_update_theme_color)
def check_font_styles(self, interval: Union[int, float] = 0) -> bool:
if self.font_style not in list(self.theme_cls.font_styles.keys()):
@@ -353,7 +724,8 @@ class MDLabel(DeclarativeBehavior, ThemableBehavior, Label, MDAdaptiveWidget):
if self.check_font_styles() is True:
font_info = self.theme_cls.font_styles[self.font_style]
self.font_name = font_info[0]
- self.font_size = sp(font_info[1])
+ if self.font_style in list(self.theme_cls.font_styles.keys())[0:14]:
+ self.font_size = sp(font_info[1])
if font_info[2] and self.can_capitalize:
self._capitalizing = True
@@ -363,6 +735,64 @@ class MDLabel(DeclarativeBehavior, ThemableBehavior, Label, MDAdaptiveWidget):
# TODO: Add letter spacing change
# self.letter_spacing = font_info[3]
+ def do_selection(self) -> None:
+ if not self.is_selected:
+ self.md_bg_color = (
+ self.theme_cls.primary_light
+ if not self.color_selection
+ else self.color_selection
+ )
+
+ def cancel_selection(self) -> None:
+ if self.is_selected:
+ self.md_bg_color = (
+ self.theme_cls.bg_normal
+ if not self.color_deselection
+ else self.color_deselection
+ )
+ self.dispatch("on_cancel_selection")
+ self.is_selected = False
+
+ def on_double_tap(self, touch, *args) -> None:
+ if self.allow_copy and self.collide_point(*touch.pos):
+ Clipboard.copy(self.text)
+ self.dispatch("on_copy")
+ if self.allow_selection and self.collide_point(*touch.pos):
+ self.do_selection()
+ self.dispatch("on_selection")
+ self.is_selected = True
+
+ def on_window_touch(self, *args):
+ if self.is_selected:
+ self.cancel_selection()
+
+ def on_copy(self, *args) -> None:
+ """
+ Called when double-tapping on the label.
+
+ .. versionadded:: 1.2.0
+ """
+
+ def on_selection(self, *args) -> None:
+ """
+ Called when double-tapping on the label.
+
+ .. versionadded:: 1.2.0
+ """
+
+ def on_cancel_selection(self, *args) -> None:
+ """
+ Called when the highlighting is removed from the label text.
+
+ .. versionadded:: 1.2.0
+ """
+
+ def on_allow_selection(self, instance_label, selection: bool) -> None:
+ if selection:
+ Window.bind(on_touch_down=self.on_window_touch)
+ else:
+ Window.unbind(on_touch_down=self.on_window_touch)
+
def on_theme_text_color(
self, instance_label, theme_text_color: str
) -> None:
@@ -414,6 +844,7 @@ class MDLabel(DeclarativeBehavior, ThemableBehavior, Label, MDAdaptiveWidget):
def on_md_bg_color(self, instance_label, color: Union[list, str]) -> None:
self.canvas.remove_group("Background_instruction")
+ self.canvas.before.clear()
with self.canvas.before:
Color(rgba=color)
self.canvas_bg = Rectangle(pos=self.pos, size=self.size)
@@ -445,6 +876,13 @@ class MDLabel(DeclarativeBehavior, ThemableBehavior, Label, MDAdaptiveWidget):
class MDIcon(MDFloatLayout, MDLabel):
+ """
+ Icon class.
+
+ For more information, see in the :class:`~MDLabel` and
+ :class:`~kivymd.uix.floatlayout.MDFloatLayout` classes documentation.
+ """
+
icon = StringProperty("android")
"""
Label icon name.
@@ -465,7 +903,7 @@ class MDIcon(MDFloatLayout, MDLabel):
badge_icon_color = ColorProperty([1, 1, 1, 1])
"""
- Badge icon color in (r, g, b, a) format.
+ Badge icon color in (r, g, b, a) or string format.
.. versionadded:: 1.0.0
@@ -475,7 +913,7 @@ class MDIcon(MDFloatLayout, MDLabel):
badge_bg_color = ColorProperty(None)
"""
- Badge icon background color in (r, g, b, a) format.
+ Badge icon background color in (r, g, b, a) or string format.
.. versionadded:: 1.0.0

diff --git a/sbapp/kivymd/uix/list/list.py b/sbapp/kivymd/uix/list/list.py
index 9f02cabd..035ca8a1 100755
--- a/sbapp/kivymd/uix/list/list.py
+++ b/sbapp/kivymd/uix/list/list.py
@@ -984,6 +984,9 @@ class MDList(MDGridLayout):
When adding (or removing) a widget, it will resize itself to fit its
children, plus top and bottom paddings as described by the `MD` spec.
+
+ For more information, see in the
+ :class:`~kivymd.uix.gridlayout.MDGridLayout` classes documentation.
"""
_list_vertical_padding = NumericProperty("8dp")
@@ -1002,6 +1005,13 @@ class BaseListItem(
):
"""
Base class to all ListItems. Not supposed to be instantiated on its own.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~kivy.uix.floatlayout.FloatLayout` classes documentation.
"""
text = StringProperty()
@@ -1242,7 +1252,12 @@ class IRightBodyTouch:
class OneLineListItem(BaseListItem):
- """A one line list item."""
+ """
+ A one line list item.
+
+ For more information, see in the :class:`~BaseListItem`
+ classes documentation.
+ """
_txt_top_pad = NumericProperty("16dp")
_txt_bot_pad = NumericProperty("15dp")
@@ -1255,7 +1270,12 @@ class OneLineListItem(BaseListItem):
class TwoLineListItem(BaseListItem):
- """A two line list item."""
+ """
+ A two line list item.
+
+ For more information, see in the :class:`~BaseListItem`
+ classes documentation.
+ """
_txt_top_pad = NumericProperty("20dp")
_txt_bot_pad = NumericProperty("15dp")
@@ -1267,7 +1287,12 @@ class TwoLineListItem(BaseListItem):
class ThreeLineListItem(BaseListItem):
- """A three line list item."""
+ """
+ A three line list item.
+
+ For more information, see in the :class:`~BaseListItem`
+ classes documentation.
+ """
_txt_top_pad = NumericProperty("16dp")
_txt_bot_pad = NumericProperty("15dp")
@@ -1280,6 +1305,13 @@ class ThreeLineListItem(BaseListItem):
class OneLineAvatarListItem(BaseListItem):
+ """
+ A one line list item with left image.
+
+ For more information, see in the :class:`~BaseListItem`
+ classes documentation.
+ """
+
_txt_left_pad = NumericProperty("72dp")
_txt_top_pad = NumericProperty("20dp")
_txt_bot_pad = NumericProperty("19dp")
@@ -1292,6 +1324,13 @@ class OneLineAvatarListItem(BaseListItem):
class TwoLineAvatarListItem(OneLineAvatarListItem):
+ """
+ A two line list item with left image.
+
+ For more information, see in the :class:`~OneLineAvatarListItem`
+ classes documentation.
+ """
+
_txt_top_pad = NumericProperty("20dp")
_txt_bot_pad = NumericProperty("15dp")
_height = NumericProperty()
@@ -1303,6 +1342,13 @@ class TwoLineAvatarListItem(OneLineAvatarListItem):
class ThreeLineAvatarListItem(ThreeLineListItem):
+ """
+ A three line list item with left image.
+
+ For more information, see in the :class:`~ThreeLineListItem`
+ classes documentation.
+ """
+
_txt_left_pad = NumericProperty("72dp")
def __init__(self, *args, **kwargs):
@@ -1310,10 +1356,24 @@ class ThreeLineAvatarListItem(ThreeLineListItem):
class OneLineIconListItem(OneLineListItem):
+ """
+ A one line list item with left icon.
+
+ For more information, see in the :class:`~OneLineListItem`
+ classes documentation.
+ """
+
_txt_left_pad = NumericProperty("72dp")
class TwoLineIconListItem(OneLineIconListItem):
+ """
+ A two line list item with left icon.
+
+ For more information, see in the :class:`~OneLineIconListItem`
+ classes documentation.
+ """
+
_txt_top_pad = NumericProperty("20dp")
_txt_bot_pad = NumericProperty("15dp")
_height = NumericProperty()
@@ -1325,10 +1385,24 @@ class TwoLineIconListItem(OneLineIconListItem):
class ThreeLineIconListItem(ThreeLineListItem):
+ """
+ A three line list item with left icon.
+
+ For more information, see in the :class:`~ThreeLineListItem`
+ classes documentation.
+ """
+
_txt_left_pad = NumericProperty("72dp")
class OneLineRightIconListItem(OneLineListItem):
+ """
+ A one line list item with right icon/image.
+
+ For more information, see in the :class:`~OneLineListItem`
+ classes documentation.
+ """
+
_txt_right_pad = NumericProperty("40dp")
def __init__(self, *args, **kwargs):
@@ -1337,6 +1411,13 @@ class OneLineRightIconListItem(OneLineListItem):
class TwoLineRightIconListItem(OneLineRightIconListItem):
+ """
+ A two line list item with right icon/image.
+
+ For more information, see in the :class:`~OneLineRightIconListItem`
+ classes documentation.
+ """
+
_txt_top_pad = NumericProperty("20dp")
_txt_bot_pad = NumericProperty("15dp")
_height = NumericProperty()
@@ -1348,6 +1429,13 @@ class TwoLineRightIconListItem(OneLineRightIconListItem):
class ThreeLineRightIconListItem(ThreeLineListItem):
+ """
+ A three line list item with right icon/image.
+
+ For more information, see in the :class:`~ThreeLineRightIconListItem`
+ classes documentation.
+ """
+
_txt_right_pad = NumericProperty("40dp")
def __init__(self, **kwargs):
@@ -1356,6 +1444,13 @@ class ThreeLineRightIconListItem(ThreeLineListItem):
class OneLineAvatarIconListItem(OneLineAvatarListItem):
+ """
+ A one line list item with left/right icon/image/widget.
+
+ For more information, see in the :class:`~OneLineAvatarListItem`
+ classes documentation.
+ """
+
_txt_right_pad = NumericProperty("40dp")
def __init__(self, *args, **kwargs):
@@ -1364,6 +1459,13 @@ class OneLineAvatarIconListItem(OneLineAvatarListItem):
class TwoLineAvatarIconListItem(TwoLineAvatarListItem):
+ """
+ A two line list item with left/right icon/image/widget.
+
+ For more information, see in the :class:`~TwoLineAvatarListItem`
+ classes documentation.
+ """
+
_txt_right_pad = NumericProperty("40dp")
def __init__(self, *args, **kwargs):
@@ -1372,6 +1474,13 @@ class TwoLineAvatarIconListItem(TwoLineAvatarListItem):
class ThreeLineAvatarIconListItem(ThreeLineAvatarListItem):
+ """
+ A three line list item with left/right icon/image/widget.
+
+ For more information, see in the :class:`~ThreeLineAvatarListItem`
+ classes documentation.
+ """
+
_txt_right_pad = NumericProperty("40dp")
def __init__(self, *args, **kwargs):
@@ -1388,13 +1497,31 @@ class TouchBehavior:
class ImageLeftWidget(
CircularRippleBehavior, ButtonBehavior, ILeftBodyTouch, FitImage
):
- pass
+ """
+ The widget implements the left image for use in ListItem classes.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~ILeftBodyTouch` and
+ :class:`~kivymd.uix.fitimage.FitImage` classes documentation.
+ """
class ImageLeftWidgetWithoutTouch(
CircularRippleBehavior, TouchBehavior, ButtonBehavior, ILeftBody, FitImage
):
"""
+ Disables the image event.
+ The widget implements the left image for use in `ListItem` classes.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and
+ :class:`~TouchBehavior` and
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~ILeftBody` and
+ :class:`~kivymd.uix.fitimage.FitImage` classes documentation.
+
.. versionadded:: 1.0.0
"""
@@ -1404,13 +1531,31 @@ class ImageLeftWidgetWithoutTouch(
class ImageRightWidget(
CircularRippleBehavior, ButtonBehavior, IRightBodyTouch, FitImage
):
- pass
+ """
+ The widget implements the right image for use in ListItem classes.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~IRightBodyTouch` and
+ :class:`~kivymd.uix.fitimage.FitImage` classes documentation.
+ """
class ImageRightWidgetWithoutTouch(
CircularRippleBehavior, TouchBehavior, ButtonBehavior, IRightBody, FitImage
):
"""
+ Disables the image event.
+ The widget implements the right image for use in `ListItem` classes.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and
+ :class:`~TouchBehavior` and
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~IRightBody` and
+ :class:`~kivymd.uix.fitimage.FitImage` classes documentation.
+
.. versionadded:: 1.0.0
"""
@@ -1418,11 +1563,29 @@ class ImageRightWidgetWithoutTouch(
class IconRightWidget(IRightBodyTouch, MDIconButton):
+ """
+ The widget implements the right icon for use in ListItem classes.
+
+ For more information, see in the
+ :class:`~IRightBodyTouch` and
+ :class:`~kivymd.uix.button.MDIconButton`
+ classes documentation.
+ """
+
pos_hint = {"center_y": 0.5}
class IconRightWidgetWithoutTouch(TouchBehavior, IRightBody, MDIconButton):
"""
+ Disables the icon event.
+ The widget implements the right icon for use in ListItem classes.
+
+ For more information, see in the
+ :class:`~TouchBehavior` and
+ :class:`~IRightBody` and
+ :class:`~kivymd.uix.button.MDIconButton`
+ classes documentation.
+
.. versionadded:: 1.0.0
"""
@@ -1431,11 +1594,29 @@ class IconRightWidgetWithoutTouch(TouchBehavior, IRightBody, MDIconButton):
class IconLeftWidget(ILeftBodyTouch, MDIconButton):
+ """
+ The widget implements the left icon for use in ListItem classes.
+
+ For more information, see in the
+ :class:`~ILeftBodyTouch` and
+ :class:`~kivymd.uix.button.MDIconButton`
+ classes documentation.
+ """
+
pos_hint = {"center_y": 0.5}
class IconLeftWidgetWithoutTouch(TouchBehavior, ILeftBody, MDIconButton):
"""
+ Disables the icon event.
+ The widget implements the left icon for use in ListItem classes.
+
+ For more information, see in the
+ :class:`~TouchBehavior` and
+ :class:`~ILeftBody` and
+ :class:`~kivymd.uix.button.MDIconButton`
+ classes documentation.
+
.. versionadded:: 1.0.0
"""
@@ -1444,4 +1625,11 @@ class IconLeftWidgetWithoutTouch(TouchBehavior, ILeftBody, MDIconButton):
class CheckboxLeftWidget(ILeftBodyTouch, MDCheckbox):
- pass
+ """
+ The widget implements the left checkbox element for use in ListItem classes.
+
+ For more information, see in the
+ :class:`~ILeftBodyTouch` and
+ :class:`~kivymd.uix.selectioncontrol.MDCheckbox`
+ classes documentation.
+ """

diff --git a/sbapp/kivymd/uix/menu/menu.kv b/sbapp/kivymd/uix/menu/menu.kv
index f72f87d4..a53c0715 100644
--- a/sbapp/kivymd/uix/menu/menu.kv
+++ b/sbapp/kivymd/uix/menu/menu.kv
@@ -1,26 +1,9 @@
-#:import STANDARD_INCREMENT kivymd.material_resources.STANDARD_INCREMENT
-
-
-<RightContent>
- adaptive_width: True
-
-
-<MDMenuItemIcon>
-
- IconLeftWidget:
- id: icon_widget
- icon: root.icon
-
-
<MDMenu>
- size_hint: None, None
- width: root.width_mult * STANDARD_INCREMENT
bar_width: 0
key_viewclass: "viewclass"
key_size: "height"
RecycleBoxLayout:
- padding: 0, "4dp", 0, "4dp"
default_size: None, dp(48)
default_size_hint: 1, None
size_hint_y: None
@@ -28,32 +11,478 @@
orientation: "vertical"
-<MenuContainer@MDCard>
+<MDDropdownTrailingTextItem>
+ orientation: "vertical"
+ MDBoxLayout:
+ id: container
+ spacing: "12dp"
+ padding: "12dp", 0, "12dp", 0
-<MDDropdownMenu>
+ MDLabel:
+ text: root.text
+ pos_hint: {"center_y": .5}
+ theme_text_color: "Custom" if root.text_color else "Primary"
+ shorten: True
+ shorten_from: "right"
+ size_hint_x: None
+ width:
+ root.width - \
+ ( \
+ + trailing_container.width \
+ + container.padding[0] \
+ + container.padding[2] \
+ + container.spacing \
+ )
+ text_color:
+ root.text_color \
+ if root.text_color else \
+ app.theme_cls.text_color
- MenuContainer:
- id: card
- orientation: "vertical"
- elevation: root.elevation
+ MDTrailingTextContainer:
+ id: trailing_container
+ text: root.trailing_text
+ adaptive_width: True
+ theme_text_color: "Custom" if root.trailing_text_color else "Primary"
+ text_color:
+ root.trailing_text_color \
+ if root.trailing_text_color else \
+ app.theme_cls.text_color
+
+ MDSeparator:
+ md_bg_color:
+ ( \
+ self.theme_cls.divider_color \
+ if not root.divider_color \
+ else root.divider_color \
+ ) \
+ if root.divider else \
+ (0, 0, 0, 0)
+
+
+<MDDropdownLeadingIconTrailingTextItem>
+ orientation: "vertical"
+
+ MDBoxLayout:
+ id: container
+ spacing: "12dp"
+ padding: "10dp", 0, "16dp", 0
+
+ MDIcon:
+ id: leading_icon
+ icon: root.leading_icon
+ size_hint: None, None
+ size: "48dp", "48dp"
+ pos_hint: {"center_y": .5}
+ theme_text_color: "Custom" if root.leading_icon_color else "Primary"
+ text_color:
+ root.leading_icon_color \
+ if root.leading_icon_color else \
+ app.theme_cls.text_color
+
+ MDLabel:
+ text: root.text
+ pos_hint: {"center_y": .5}
+ theme_text_color: "Custom" if root.text_color else "Primary"
+ shorten: True
+ shorten_from: "right"
+ size_hint_x: None
+ width:
+ root.width - \
+ ( \
+ leading_icon.width \
+ + trailing_container.width \
+ + container.padding[0] \
+ + container.padding[2] \
+ + container.spacing \
+ + dp(18) \
+ )
+ text_color:
+ root.text_color \
+ if root.text_color else \
+ app.theme_cls.text_color
+
+ Widget:
+
+ MDTrailingTextContainer:
+ id: trailing_container
+ text: root.trailing_text
+ adaptive_width: True
+ theme_text_color: "Custom" if root.trailing_text_color else "Primary"
+ text_color:
+ root.trailing_text_color \
+ if root.trailing_text_color else \
+ app.theme_cls.text_color
+
+ MDSeparator:
+ md_bg_color:
+ ( \
+ self.theme_cls.divider_color \
+ if not root.divider_color \
+ else root.divider_color \
+ ) \
+ if root.divider else \
+ (0, 0, 0, 0)
+
+
+<MDDropdownTrailingIconItem>
+ orientation: "vertical"
+
+ MDBoxLayout:
+ id: container
+ spacing: "12dp"
+ padding: "12dp", 0, "12dp", 0
+
+ MDLabel:
+ id: label
+ text: root.text
+ shorten: True
+ size_hint_x: None
+ shorten_from: "right"
+ pos_hint: {"center_y": .5}
+ theme_text_color: "Custom" if root.text_color else "Primary"
+ shorten: True
+ shorten_from: "right"
+ width:
+ root.width - \
+ ( \
+ + trailing_icon.width \
+ + container.padding[0] \
+ + container.padding[2] \
+ + container.spacing \
+ + dp(18) \
+ )
+ text_color:
+ root.text_color \
+ if root.text_color else \
+ app.theme_cls.text_color
+
+ Widget:
+
+ MDIcon:
+ id: trailing_icon
+ size_hint: None, None
+ size: "48dp", "48dp"
+ pos_hint: {"center_y": .5}
+ icon: root.trailing_icon
+ theme_text_color: "Custom" if root.trailing_icon_color else "Primary"
+ text_color:
+ root.trailing_icon_color \
+ if root.trailing_icon_color else \
+ app.theme_cls.text_color
+
+ MDSeparator:
+ md_bg_color:
+ ( \
+ self.theme_cls.divider_color \
+ if not root.divider_color \
+ else root.divider_color \
+ ) \
+ if root.divider else \
+ (0, 0, 0, 0)
+
+
+<MDTrailingIconTextContainer>
+ adaptive_width: True
+
+ MDIcon:
+ icon: root.trailing_icon
size_hint: None, None
- size: md_menu.size[0], md_menu.size[1] + content_header.height
- pos: md_menu.pos
- opacity: md_menu.opacity
- radius: root.radius
- md_bg_color:
- root.background_color \
- if root.background_color else root.theme_cls.bg_dark
-
- MDBoxLayout:
- id: content_header
- adaptive_size: True
-
- MDMenu:
- id: md_menu
- drop_cls: root
- width_mult: root.width_mult
+ size: "48dp", "48dp"
+ pos_hint: {"center_y": .5}
+ theme_text_color: "Custom" if root.trailing_icon_color else "Primary"
+ text_color:
+ root.trailing_icon_color \
+ if root.trailing_icon_color else \
+ app.theme_cls.text_color
+
+ MDLabel:
+ text: root.trailing_text
+ adaptive_size: True
+ pos_hint: {"center_y": .5}
+ theme_text_color: "Custom" if root.trailing_text_color else "Primary"
+ text_color:
+ root.trailing_text_color \
+ if root.trailing_text_color else \
+ app.theme_cls.text_color
+
+
+<MDDropdownTrailingIconTextItem>
+ orientation: "vertical"
+
+ MDBoxLayout:
+ id: container
+ spacing: "12dp"
+ padding: "12dp", 0, "12dp", 0
+
+ MDLabel:
+ id: label
+ text: root.text
+ shorten: True
+ size_hint_x: None
+ shorten_from: "right"
+ pos_hint: {"center_y": .5}
+ theme_text_color: "Custom" if root.text_color else "Primary"
+ shorten: True
+ shorten_from: "right"
+ width:
+ root.width - \
+ ( \
+ + trailing_container.width \
+ + container.padding[0] \
+ + container.padding[2] \
+ + container.spacing \
+ )
+ text_color:
+ root.text_color \
+ if root.text_color else \
+ app.theme_cls.text_color
+
+ MDTrailingIconTextContainer:
+ id: trailing_container
+ trailing_icon: root.trailing_icon
+ trailing_text: root.trailing_text
+ trailing_text_color: root.trailing_text_color
+ trailing_icon_color: root.trailing_icon_color
+
+ MDSeparator:
+ md_bg_color:
+ ( \
+ self.theme_cls.divider_color \
+ if not root.divider_color \
+ else root.divider_color \
+ ) \
+ if root.divider else \
+ (0, 0, 0, 0)
+
+
+<MDDropdownTextItem>
+ orientation: "vertical"
+
+ MDLabel:
+ text: root.text
+ valign: "center"
+ padding_x: "12dp"
+ theme_text_color: "Custom" if root.text_color else "Primary"
+ shorten: True
+ shorten_from: "right"
+ text_color:
+ root.text_color \
+ if root.text_color else \
+ app.theme_cls.text_color
+
+ MDSeparator:
+ md_bg_color:
+ ( \
+ self.theme_cls.divider_color \
+ if not root.divider_color \
+ else root.divider_color \
+ ) \
+ if root.divider else \
+ (0, 0, 0, 0)
+
+
+<MDDropdownLeadingTrailingIconTextItem>
+ orientation: "vertical"
+
+ MDBoxLayout:
+ id: container
+ spacing: "12dp"
+ padding: "10dp", 0, "16dp", 0
+
+ MDIcon:
+ id: leading_icon
+ icon: root.leading_icon
+ size_hint: None, None
+ size: "48dp", "48dp"
+ pos_hint: {"center_y": .5}
+ theme_text_color: "Custom" if root.leading_icon_color else "Primary"
+ text_color:
+ root.leading_icon_color \
+ if root.leading_icon_color else \
+ app.theme_cls.text_color
+
+ MDLabel:
+ text: root.text
+ pos_hint: {"center_y": .5}
+ theme_text_color: "Custom" if root.text_color else "Primary"
+ shorten: True
+ shorten_from: "right"
+ size_hint_x: None
+ width:
+ root.width - \
+ ( \
+ leading_icon.width \
+ + trailing_container.width \
+ + container.padding[0] \
+ + container.padding[2] \
+ + container.spacing \
+ + dp(18) \
+ )
+ text_color:
+ root.text_color \
+ if root.text_color else \
+ app.theme_cls.text_color
+
+ Widget:
+
+ MDTrailingIconTextContainer:
+ id: trailing_container
+ trailing_icon: root.trailing_icon
+ trailing_text: root.trailing_text
+ trailing_icon_color: root.trailing_icon_color
+ trailing_text_color: root.trailing_text_color
+
+ MDSeparator:
+ md_bg_color:
+ ( \
+ self.theme_cls.divider_color \
+ if not root.divider_color \
+ else root.divider_color \
+ ) \
+ if root.divider else \
+ (0, 0, 0, 0)
+
+
+<MDDropdownLeadingTrailingIconItem>
+ orientation: "vertical"
+
+ MDBoxLayout:
+ id: container
+ spacing: "12dp"
+ padding: "10dp", 0, "12dp", 0
+
+ MDIcon:
+ id: leading_icon
+ icon: root.leading_icon
size_hint: None, None
- size: 0, 0
- opacity: 0
+ size: "48dp", "48dp"
+ pos_hint: {"center_y": .5}
+ theme_text_color: "Custom" if root.leading_icon_color else "Primary"
+ text_color:
+ root.leading_icon_color \
+ if root.leading_icon_color else \
+ app.theme_cls.text_color
+
+ MDLabel:
+ id: label
+ text: root.text
+ shorten: True
+ size_hint_x: None
+ shorten_from: "right"
+ pos_hint: {"center_y": .5}
+ theme_text_color: "Custom" if root.text_color else "Primary"
+ shorten: True
+ shorten_from: "right"
+ width:
+ root.width - \
+ ( \
+ leading_icon.width \
+ + trailing_icon.width \
+ + container.padding[0] \
+ + container.padding[2] \
+ + container.spacing \
+ + dp(18) \
+ )
+ text_color:
+ root.text_color \
+ if root.text_color else \
+ app.theme_cls.text_color
+
+ Widget:
+
+ MDIcon:
+ id: trailing_icon
+ size_hint: None, None
+ size: "48dp", "48dp"
+ pos_hint: {"center_y": .5}
+ icon: root.trailing_icon
+ theme_text_color: "Custom" if root.trailing_icon_color else "Primary"
+ text_color:
+ root.trailing_icon_color \
+ if root.trailing_icon_color else \
+ app.theme_cls.text_color
+
+ MDSeparator:
+ md_bg_color:
+ ( \
+ self.theme_cls.divider_color \
+ if not root.divider_color \
+ else root.divider_color \
+ ) \
+ if root.divider else \
+ (0, 0, 0, 0)
+
+
+<MDDropdownLeadingIconItem>
+ orientation: "vertical"
+
+ MDBoxLayout:
+ id: container
+ spacing: "12dp"
+ padding: "12dp", 0, "12dp", 0
+
+ MDIcon:
+ id: leading_icon
+ icon: root.leading_icon
+ size_hint: None, None
+ size: "48dp", "48dp"
+ pos_hint: {"center_y": .5}
+ theme_text_color: "Custom" if root.leading_icon_color else "Primary"
+ text_color:
+ root.leading_icon_color \
+ if root.leading_icon_color else \
+ app.theme_cls.text_color
+
+ MDLabel:
+ id: label
+ text: root.text
+ shorten: True
+ size_hint_x: None
+ shorten_from: "right"
+ pos_hint: {"center_y": .5}
+ theme_text_color: "Custom" if root.text_color else "Primary"
+ shorten: True
+ shorten_from: "right"
+ width:
+ root.width - \
+ ( \
+ leading_icon.width \
+ + container.padding[0] \
+ + container.padding[2] \
+ + container.spacing \
+ )
+ text_color:
+ root.text_color \
+ if root.text_color else \
+ app.theme_cls.text_color
+
+ MDSeparator:
+ md_bg_color:
+ ( \
+ self.theme_cls.divider_color \
+ if not root.divider_color \
+ else root.divider_color \
+ ) \
+ if root.divider else \
+ (0, 0, 0, 0)
+
+
+<MDDropdownMenu>
+ orientation: "vertical"
+ elevation: root.elevation
+ shadow_radius: root.shadow_radius
+ shadow_softness: root.shadow_softness
+ shadow_offset: root.shadow_offset
+ shadow_color: root.shadow_color
+ shadow_color: root.shadow_color
+ radius: root.radius
+ size_hint: None, None
+
+ MDBoxLayout:
+ id: content_header
+ adaptive_size: True
+
+ MDMenu:
+ id: md_menu
+ drop_cls: root

diff --git a/sbapp/kivymd/uix/menu/menu.py b/sbapp/kivymd/uix/menu/menu.py
index 102cf96f..53d6b13e 100755
--- a/sbapp/kivymd/uix/menu/menu.py
+++ b/sbapp/kivymd/uix/menu/menu.py
@@ -4,19 +4,24 @@ Components/Menu
.. seealso::
- `Material Design spec, Menus <https://material.io/components/menus>`_
+ `Material Design spec, Menus <https://m3.material.io/components/menus/overview>`_
.. rubric:: Menus display a list of choices on temporary surfaces.
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-previous.png
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-preview.png
:align: center
+- Menus should be easy to open, close, and interact with
+- Menu content should be suited to user needs
+- Menu items should be easy to scan
+
Usage
-----
.. code-block:: python
from kivy.lang import Builder
+ from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
@@ -26,34 +31,31 @@ Usage
MDRaisedButton:
id: button
- text: "PRESS ME"
+ text: "Press me"
pos_hint: {"center_x": .5, "center_y": .5}
- on_release: app.menu.open()
+ on_release: app.menu_open()
'''
class Test(MDApp):
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.screen = Builder.load_string(KV)
+ def menu_open(self):
menu_items = [
{
"text": f"Item {i}",
- "viewclass": "OneLineListItem",
"on_release": lambda x=f"Item {i}": self.menu_callback(x),
} for i in range(5)
]
- self.menu = MDDropdownMenu(
- caller=self.screen.ids.button,
- items=menu_items,
- width_mult=4,
- )
+ MDDropdownMenu(
+ caller=self.root.ids.button, items=menu_items
+ ).open()
def menu_callback(self, text_item):
print(text_item)
def build(self):
- return self.screen
+ self.theme_cls.primary_palette = "Orange"
+ self.theme_cls.theme_style = "Dark"
+ return Builder.load_string(KV)
Test().run()
@@ -61,116 +63,145 @@ Usage
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-usage.gif
:align: center
-.. Warning:: Do not create the :class:`~MDDropdownMenu` object when you open
- the menu window. Because on a mobile device this one will be very slow!
+Anatomy
+-------
-Wrong
------
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-anatomy.png
+ :align: center
-.. code-block:: python
+You can combine the following parameters:
+-----------------------------------------
+
+- leading_icon
+- text
+- trailing_icon
+- trailing_text
- menu = MDDropdownMenu(caller=self.screen.ids.button, items=menu_items)
- menu.open()
+...to create the necessary types of menu items:
+
+.. code-block:: python
-Customization of menu item
---------------------------
+ menu_items = [
+ {
+ "text": "Strikethrough",
+ "leading_icon": "check",
+ "trailing_icon": "apple-keyboard-command",
+ "trailing_text": "+Shift+X",
+ }
+ ]
-Menu items are created in the same way as items for the
-:class:`~kivy.uix.recycleview.RecycleView` class.
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-leading-icon-trailing-icon-trailing-text.png
+ :align: center
.. code-block:: python
- from kivy.lang import Builder
- from kivy.metrics import dp
- from kivy.properties import StringProperty
+ menu_items = [
+ {
+ "text": "Strikethrough",
+ "trailing_icon": "apple-keyboard-command",
+ "trailing_text": "+Shift+X",
+ }
+ ]
- from kivymd.app import MDApp
- from kivymd.uix.boxlayout import MDBoxLayout
- from kivymd.uix.list import IRightBodyTouch, OneLineAvatarIconListItem
- from kivymd.uix.menu import MDDropdownMenu
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-trailing-icon-trailing-text.png
+ :align: center
- KV = '''
- <RightContentCls>
- disabled: True
- adaptive_size: True
- pos_hint: {"center_y": .5}
+.. code-block:: python
- MDIconButton:
- icon: root.icon
- user_font_size: "16sp"
- md_bg_color_disabled: 0, 0, 0, 0
+ menu_items = [
+ {
+ "text": "Strikethrough",
+ "trailing_icon": "apple-keyboard-command",
+ }
+ ]
- MDLabel:
- text: root.text
- font_style: "Caption"
- adaptive_size: True
- pos_hint: {"center_y": .5}
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-trailing-icon.png
+ :align: center
+.. code-block:: python
- <Item>
+ menu_items = [
+ {
+ "text": "Strikethrough",
+ "trailing_text": "Shift+X",
+ }
+ ]
- IconLeftWidget:
- icon: root.left_icon
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-trailing-text.png
+ :align: center
- RightContentCls:
- id: container
- icon: root.right_icon
- text: root.right_text
+.. code-block:: python
+ menu_items = [
+ {
+ "text": "Strikethrough",
+ "leading_icon": "check",
+ "trailing_icon": "apple-keyboard-command",
+ }
+ ]
- MDScreen:
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-leading-icon-trailing-icon.png
+ :align: center
- MDRaisedButton:
- id: button
- text: "PRESS ME"
- pos_hint: {"center_x": .5, "center_y": .5}
- on_release: app.menu.open()
- '''
+.. code-block:: python
+ menu_items = [
+ {
+ "text": "Strikethrough",
+ "leading_icon": "check",
+ }
+ ]
- class RightContentCls(IRightBodyTouch, MDBoxLayout):
- icon = StringProperty()
- text = StringProperty()
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-leading-icon.png
+ :align: center
+.. code-block:: python
- class Item(OneLineAvatarIconListItem):
- left_icon = StringProperty()
- right_icon = StringProperty()
- right_text = StringProperty()
+ menu_items = [
+ {
+ "text": "Strikethrough",
+ "leading_icon": "check",
+ "trailing_text": "Shift+X",
+ }
+ ]
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-leading-icon-trailing-text.png
+ :align: center
- class Test(MDApp):
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.screen = Builder.load_string(KV)
- menu_items = [
- {
- "text": f"Item {i}",
- "right_text": f"R+{i}",
- "right_icon": "apple-keyboard-command",
- "left_icon": "git",
- "viewclass": "Item",
- "height": dp(54),
- "on_release": lambda x=f"Item {i}": self.menu_callback(x),
- } for i in range(5)
- ]
- self.menu = MDDropdownMenu(
- caller=self.screen.ids.button,
- items=menu_items,
- width_mult=4,
- )
+.. code-block:: python
- def menu_callback(self, text_item):
- print(text_item)
+ menu_items = [
+ {
+ "text": "Strikethrough",
+ }
+ ]
- def build(self):
- return self.screen
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-text.png
+ :align: center
+You can use the following parameters to customize the menu items:
+-----------------------------------------------------------------
- Test().run()
+- text_color
+- leading_icon_color
+- trailing_icon_color
+- trailing_text_color
+.. code-block:: python
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-right.gif
+ menu_items = [
+ {
+ "text": "Strikethrough",
+ "leading_icon": "check",
+ "trailing_icon": "apple-keyboard-command",
+ "trailing_text": "+Shift+X",
+ "leading_icon_color": "orange",
+ "trailing_icon_color": "green",
+ "trailing_text_color": "red",
+ }
+ ]
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-customize.png
:align: center
.. Header:
@@ -188,22 +219,18 @@ Header
KV = '''
<MenuHeader>
- orientation: "vertical"
- adaptive_size: True
+ spacing: "12dp"
padding: "4dp"
+ adaptive_height: True
- MDBoxLayout:
- spacing: "12dp"
- adaptive_size: True
-
- MDIconButton:
- icon: "gesture-tap-button"
- pos_hint: {"center_y": .5}
+ MDIconButton:
+ icon: "gesture-tap-button"
+ pos_hint: {"center_y": .5}
- MDLabel:
- text: "Actions"
- adaptive_size: True
- pos_hint: {"center_y": .5}
+ MDLabel:
+ text: "Actions"
+ adaptive_size: True
+ pos_hint: {"center_y": .5}
MDScreen:
@@ -227,8 +254,6 @@ Header
menu_items = [
{
"text": f"Item {i}",
- "viewclass": "OneLineListItem",
- "height": dp(56),
"on_release": lambda x=f"Item {i}": self.menu_callback(x),
} for i in range(5)
]
@@ -236,13 +261,14 @@ Header
header_cls=MenuHeader(),
caller=self.screen.ids.button,
items=menu_items,
- width_mult=4,
)
def menu_callback(self, text_item):
print(text_item)
def build(self):
+ self.theme_cls.primary_palette = "Orange"
+ self.theme_cls.theme_style = "Dark"
return self.screen
@@ -257,13 +283,8 @@ Menu with MDTopAppBar
The :class:`~MDDropdownMenu` works well with the standard
:class:`~kivymd.uix.toolbar.MDTopAppBar`. Since the buttons on the Toolbar are created
by the MDTopAppBar component, it is necessary to pass the button as an argument to
-the callback using `lambda x: app.callback(x)`.
-
-.. note:: This example uses drop down menus for both the righthand and
- lefthand menus (i.e both the 'triple bar' and 'triple dot' menus) to
- illustrate that it is possible. A better solution for the 'triple bar' menu
- would probably have been :class:`~kivymd.uix.MDNavigationDrawer`.
-
+the callback using `lambda x: app.callback(x)`. This example uses drop down menus
+for both the righthand and lefthand menus.
.. code-block:: python
@@ -291,18 +312,15 @@ the callback using `lambda x: app.callback(x)`.
class Test(MDApp):
def build(self):
+ self.theme_cls.primary_palette = "Orange"
+ self.theme_cls.theme_style = "Dark"
menu_items = [
{
- "viewclass": "OneLineListItem",
"text": f"Item {i}",
- "height": dp(56),
"on_release": lambda x=f"Item {i}": self.menu_callback(x),
- } for i in range(5)
+ } for i in range(5)
]
- self.menu = MDDropdownMenu(
- items=menu_items,
- width_mult=4,
- )
+ self.menu = MDDropdownMenu(items=menu_items)
return Builder.load_string(KV)
def callback(self, button):
@@ -316,7 +334,7 @@ the callback using `lambda x: app.callback(x)`.
Test().run()
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-menu.gif
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-menu.png
:align: center
.. Position:
@@ -334,20 +352,12 @@ Bottom position
from kivy.lang import Builder
from kivy.metrics import dp
- from kivy.properties import StringProperty
- from kivymd.uix.list import OneLineIconListItem
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
KV = '''
- <IconListItem>
-
- IconLeftWidget:
- icon: root.icon
-
-
- MDScreen
+ MDScreen:
MDTextField:
id: field
@@ -359,19 +369,12 @@ Bottom position
'''
- class IconListItem(OneLineIconListItem):
- icon = StringProperty()
-
-
class Test(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
menu_items = [
{
- "viewclass": "IconListItem",
- "icon": "git",
- "height": dp(56),
"text": f"Item {i}",
"on_release": lambda x=f"Item {i}": self.set_item(x),
} for i in range(5)]
@@ -379,20 +382,21 @@ Bottom position
caller=self.screen.ids.field,
items=menu_items,
position="bottom",
- width_mult=4,
)
- def set_item(self, text__item):
- self.screen.ids.field.text = text__item
+ def set_item(self, text_item):
+ self.screen.ids.field.text = text_item
self.menu.dismiss()
def build(self):
+ self.theme_cls.primary_palette = "Orange"
+ self.theme_cls.theme_style = "Dark"
return self.screen
Test().run()
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position.gif
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position.png
:align: center
Center position
@@ -402,20 +406,12 @@ Center position
from kivy.lang import Builder
from kivy.metrics import dp
- from kivy.properties import StringProperty
- from kivymd.uix.list import OneLineIconListItem
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
KV = '''
- <IconListItem>
-
- IconLeftWidget:
- icon: root.icon
-
-
- MDScreen
+ MDScreen:
MDDropDownItem:
id: drop_item
@@ -425,20 +421,13 @@ Center position
'''
- class IconListItem(OneLineIconListItem):
- icon = StringProperty()
-
-
class Test(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
menu_items = [
{
- "viewclass": "IconListItem",
- "icon": "git",
"text": f"Item {i}",
- "height": dp(56),
"on_release": lambda x=f"Item {i}": self.set_item(x),
} for i in range(5)
]
@@ -446,7 +435,6 @@ Center position
caller=self.screen.ids.drop_item,
items=menu_items,
position="center",
- width_mult=4,
)
self.menu.bind()
@@ -455,6 +443,8 @@ Center position
self.menu.dismiss()
def build(self):
+ self.theme_cls.primary_palette = "Orange"
+ self.theme_cls.theme_style = "Dark"
return self.screen
@@ -462,157 +452,489 @@ Center position
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position-center.gif
:align: center
-"""
-
-__all__ = ("MDDropdownMenu",)
-
-import os
-from typing import Union
-
-from kivy.animation import Animation
-from kivy.clock import Clock
-from kivy.core.window import Window
-from kivy.core.window.window_sdl2 import WindowSDL
-from kivy.lang import Builder
-from kivy.metrics import dp
-from kivy.properties import (
- ColorProperty,
- ListProperty,
- NumericProperty,
- ObjectProperty,
- OptionProperty,
- StringProperty,
- VariableListProperty,
-)
-from kivy.uix.floatlayout import FloatLayout
-from kivy.uix.recycleview import RecycleView
-
-import kivymd.material_resources as m_res
-from kivymd import uix_path
-from kivymd.theming import ThemableBehavior
-
-with open(
- os.path.join(uix_path, "menu", "menu.kv"), encoding="utf-8"
-) as kv_file:
- Builder.load_string(kv_file.read())
+API break
+=========
-class MDMenu(RecycleView):
- width_mult = NumericProperty(1)
- """
- See :attr:`~MDDropdownMenu.width_mult`.
- """
-
- drop_cls = ObjectProperty()
- """
- See :class:`~MDDropdownMenu` class.
- """
+1.1.1 version
+-------------
+.. code-block:: python
-class MDDropdownMenu(ThemableBehavior, FloatLayout):
- """
- :Events:
- `on_release`
- The method that will be called when you click menu items.
- """
+ from kivy.lang import Builder
+ from kivy.metrics import dp
+ from kivy.properties import StringProperty
- header_cls = ObjectProperty()
- """
- An instance of the class (`Kivy` or `KivyMD` widget) that will be added
- to the menu header.
+ from kivymd.app import MDApp
+ from kivymd.uix.boxlayout import MDBoxLayout
+ from kivymd.uix.list import IRightBodyTouch, OneLineAvatarIconListItem
+ from kivymd.uix.menu import MDDropdownMenu
- .. versionadded:: 0.104.2
+ KV = '''
+ <RightContentCls>
+ disabled: True
+ adaptive_size: True
+ pos_hint: {"center_y": .5}
- See Header_ for more information.
+ MDIconButton:
+ icon: root.icon
+ icon_size: "16sp"
+ md_bg_color_disabled: 0, 0, 0, 0
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-header-cls.png
- :align: center
+ MDLabel:
+ text: root.text
+ font_style: "Caption"
+ adaptive_size: True
+ pos_hint: {"center_y": .5}
- :attr:`header_cls` is a :class:`~kivy.properties.ObjectProperty`
- and defaults to `None`.
- """
- items = ListProperty()
- """
- See :attr:`~kivy.uix.recycleview.RecycleView.data`.
+ <Item>
- .. code-block:: python
+ IconLeftWidget:
+ icon: root.left_icon
- items = [
- {
- "viewclass": "OneLineListItem",
- "height": dp(56),
- "text": f"Item {i}",
- }
- for i in range(5)
- ]
- self.menu = MDDropdownMenu(
- items=items,
- ...,
- )
+ RightContentCls:
+ id: container
+ icon: root.right_icon
+ text: root.right_text
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-items.png
- :align: center
- :attr:`items` is a :class:`~kivy.properties.ListProperty`
- and defaults to `[]`.
- """
+ MDScreen:
- width_mult = NumericProperty(1)
- """
- This number multiplied by the standard increment ('56dp' on mobile, '64dp'
- on desktop), determines the width of the menu items.
+ MDRaisedButton:
+ id: button
+ text: "PRESS ME"
+ pos_hint: {"center_x": .5, "center_y": .5}
+ on_release: app.menu.open()
+ '''
- If the resulting number were to be too big for the application Window,
- the multiplier will be adjusted for the biggest possible one.
- .. code-block:: python
+ class RightContentCls(IRightBodyTouch, MDBoxLayout):
+ icon = StringProperty()
+ text = StringProperty()
- self.menu = MDDropdownMenu(
- width_mult=4,
- ...,
- )
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-width-mult-4.png
- :align: center
+ class Item(OneLineAvatarIconListItem):
+ left_icon = StringProperty()
+ right_icon = StringProperty()
+ right_text = StringProperty()
- .. code-block:: python
- self.menu = MDDropdownMenu(
- width_mult=8,
- ...,
- )
+ class Test(MDApp):
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.screen = Builder.load_string(KV)
+ menu_items = [
+ {
+ "text": f"Item {i}",
+ "right_text": "+Shift+X",
+ "right_icon": "apple-keyboard-command",
+ "left_icon": "web",
+ "viewclass": "Item",
+ "height": dp(54),
+ "on_release": lambda x=f"Item {i}": self.menu_callback(x),
+ } for i in range(5)
+ ]
+ self.menu = MDDropdownMenu(
+ caller=self.screen.ids.button,
+ items=menu_items,
+ bg_color="#bdc6b0",
+ width_mult=4,
+ )
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-width-mult-8.png
- :align: center
+ def menu_callback(self, text_item):
+ print(text_item)
- :attr:`width_mult` is a :class:`~kivy.properties.NumericProperty`
- and defaults to `1`.
+ def build(self):
+ return self.screen
+
+
+ Test().run()
+
+1.2.0 version
+-------------
+
+.. code-block:: python
+
+ from kivy.lang import Builder
+ from kivy.metrics import dp
+
+ from kivymd.app import MDApp
+ from kivymd.uix.menu import MDDropdownMenu
+
+ KV = '''
+ MDScreen:
+
+ MDRaisedButton:
+ id: button
+ text: "PRESS ME"
+ pos_hint: {"center_x": .5, "center_y": .5}
+ on_release: app.menu.open()
+ '''
+
+
+ class Test(MDApp):
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.screen = Builder.load_string(KV)
+ menu_items = [
+ {
+ "text": f"Item {i}",
+ "leading_icon": "web",
+ "trailing_icon": "apple-keyboard-command",
+ "trailing_text": "+Shift+X",
+ "trailing_icon_color": "grey",
+ "trailing_text_color": "grey",
+ "on_release": lambda x=f"Item {i}": self.menu_callback(x),
+ } for i in range(5)
+ ]
+ self.menu = MDDropdownMenu(
+ md_bg_color="#bdc6b0",
+ caller=self.screen.ids.button,
+ items=menu_items,
+ )
+
+ def menu_callback(self, text_item):
+ print(text_item)
+
+ def build(self):
+ return self.screen
+
+
+ Test().run()
+"""
+
+from __future__ import annotations
+
+__all__ = (
+ "BaseDropdownItem",
+ "MDDropdownMenu",
+ "MDDropdownTextItem",
+ "MDDropdownLeadingIconItem",
+ "MDDropdownTrailingIconItem",
+ "MDDropdownTrailingIconTextItem",
+ "MDDropdownTrailingTextItem",
+ "MDDropdownLeadingTrailingIconTextItem",
+ "MDDropdownLeadingIconTrailingTextItem",
+)
+
+import os
+
+from kivy.clock import Clock
+from kivy.core.window import Window
+from kivy.lang import Builder
+from kivy.metrics import dp
+from kivy.properties import (
+ ColorProperty,
+ ListProperty,
+ NumericProperty,
+ ObjectProperty,
+ OptionProperty,
+ VariableListProperty,
+ StringProperty,
+)
+from kivy.uix.recycleview import RecycleView
+
+import kivymd.material_resources as m_res
+from kivymd import uix_path
+from kivymd.uix.behaviors import StencilBehavior, RectangularRippleBehavior
+from kivymd.uix.behaviors.motion_behavior import MotionDropDownMenuBehavior
+from kivymd.uix.boxlayout import MDBoxLayout
+from kivymd.uix.card import MDCard
+from kivymd.uix.label import MDLabel
+from kivymd.uix.list import IRightBody
+
+with open(
+ os.path.join(uix_path, "menu", "menu.kv"), encoding="utf-8"
+) as kv_file:
+ Builder.load_string(kv_file.read())
+
+
+class MDMenu(RecycleView):
+ width_mult = NumericProperty(1)
"""
+ See :attr:`~MDDropdownMenu.width_mult`.
- max_height = NumericProperty()
+ .. deprecated:: 1.2.0
"""
- The menu will grow no bigger than this number. Set to 0 for no limit.
- .. code-block:: python
+ drop_cls = ObjectProperty()
+ """
+ See :class:`~MDDropdownMenu` class.
+ """
- self.menu = MDDropdownMenu(
- max_height=dp(112),
- ...,
- )
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-max-height-112.png
- :align: center
+class BaseDropdownItem(RectangularRippleBehavior, MDBoxLayout):
+ """
+ Base class for menu items.
- .. code-block:: python
+ .. versionadded:: 1.2.0
- self.menu = MDDropdownMenu(
- max_height=dp(224),
- ...,
- )
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` classes.
+ """
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-max-height-224.png
- :align: center
+ text = StringProperty()
+ """
+ The text of the menu item.
+
+ :attr:`text` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `''`.
+ """
+
+ leading_icon = StringProperty()
+ """
+ The leading icon of the menu item.
+
+ :attr:`leading_icon` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `''`.
+ """
+
+ trailing_icon = StringProperty()
+ """
+ The trailing icon of the menu item.
+
+ :attr:`trailing_icon` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `''`.
+ """
+
+ trailing_text = StringProperty()
+ """
+ The trailing text of the menu item.
+
+ :attr:`trailing_text` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `''`.
+ """
+
+ text_color = ColorProperty(None)
+ """
+ The color of the text in (r, g, b, a) or string format for the text of the
+ menu item.
+
+ :attr:`text_color` is a :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
+ """
+
+ leading_icon_color = ColorProperty(None)
+ """
+ The color of the text in (r, g, b, a) or string format for the leading icon
+ of the menu item.
+
+ :attr:`leading_icon_color` is a :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
+ """
+
+ trailing_icon_color = ColorProperty(None)
+ """
+ The color of the text in (r, g, b, a) or string format for the trailing
+ icon of the menu item.
+
+ :attr:`leading_icon_color` is a :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
+ """
+
+ trailing_text_color = ColorProperty(None)
+ """
+ The color of the text in (r, g, b, a) or string format for the trailing
+ text of the menu item.
+
+ :attr:`leading_icon_color` is a :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
+ """
+
+ divider = OptionProperty("Full", options=["Full", None], allownone=True)
+ """
+ Divider mode. Available options are: `'Full'`, `None`
+ and default to `'Full'`.
+
+ :attr:`divider` is a :class:`~kivy.properties.OptionProperty`
+ and defaults to `'Full'`.
+ """
+
+ divider_color = ColorProperty(None)
+ """
+ Divider color in (r, g, b, a) or string format.
+
+ :attr:`divider_color` is a :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
+ """
+
+
+class MDTrailingTextContainer(BaseDropdownItem, IRightBody, MDLabel):
+ """
+ Implements a container for trailing text.
+
+ .. versionadded:: 1.2.0
+
+ For more information, see in the
+ :class:`~BaseDropdownItem` and
+ :class:`~kivymd.uix.list.IRightBody` and
+ :class:`~kivymd.uix.label.MDLabel` classes.
+ """
+
+
+class MDTrailingIconTextContainer(BaseDropdownItem, IRightBody, MDBoxLayout):
+ """
+ Implements a container for trailing icons and trailing text.
+
+ .. versionadded:: 1.2.0
+
+ For more information, see in the
+ :class:`~BaseDropdownItem` and
+ :class:`~kivymd.uix.list.IRightBody` and
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` classes.
+ """
+
+
+class MDDropdownTextItem(BaseDropdownItem):
+ """
+ Implements a menu item with text without leading and trailing icons.
+
+ .. versionadded:: 1.2.0
+
+ For more information, see in the :class:`~BaseDropdownItem` class.
+ """
+
+
+class MDDropdownLeadingIconItem(BaseDropdownItem):
+ """
+ Implements a menu item with text, leading icon and without trailing icon.
+
+ .. versionadded:: 1.2.0
+
+ For more information, see in the :class:`~BaseDropdownItem` class.
+ """
+
+
+class MDDropdownTrailingIconItem(BaseDropdownItem):
+ """
+ Implements a menu item with text, without leading icon and with trailing
+ icon.
+
+ .. versionadded:: 1.2.0
+
+ For more information, see in the :class:`~BaseDropdownItem` class.
+ """
+
+
+class MDDropdownTrailingIconTextItem(BaseDropdownItem):
+ """
+ Implements a menu item with text, without leading icon, with trailing
+ icon and with trailing text.
+
+ .. versionadded:: 1.2.0
+
+ For more information, see in the :class:`~BaseDropdownItem` class.
+ """
+
+
+class MDDropdownTrailingTextItem(BaseDropdownItem):
+ """
+ Implements a menu item with text, without leading icon, without trailing
+ icon and with trailing text.
+
+ .. versionadded:: 1.2.0
+
+ For more information, see in the :class:`~BaseDropdownItem` class.
+ """
+
+
+class MDDropdownLeadingIconTrailingTextItem(BaseDropdownItem):
+ """
+ Implements a menu item with text, leading icon and with trailing text.
+
+ .. versionadded:: 1.2.0
+
+ For more information, see in the :class:`~BaseDropdownItem` class.
+ """
+
+
+class MDDropdownLeadingTrailingIconTextItem(BaseDropdownItem):
+ """
+ Implements a menu item with text, with leading icon, with trailing
+ icon and with trailing text.
+
+ .. versionadded:: 1.2.0
+
+ For more information, see in the :class:`~BaseDropdownItem` class.
+ """
+
+
+class MDDropdownLeadingTrailingIconItem(BaseDropdownItem):
+ """
+ Implements a menu item with text, with leading icon, with trailing icon.
+
+ .. versionadded:: 1.2.0
+
+ For more information, see in the :class:`~BaseDropdownItem` class.
+ """
+
+
+class MDDropdownMenu(MotionDropDownMenuBehavior, StencilBehavior, MDCard):
+ """
+ Dropdown menu class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.MotionDropDownMenuBehavior` and
+ :class:`~kivymd.uix.behaviors.StencilBehavior` and
+ :class:`~kivymd.uix.card.MDCard`
+ classes documentation.
+
+ :Events:
+ `on_release`
+ The method that will be called when you click menu items.
+ """
+
+ header_cls = ObjectProperty()
+ """
+ An instance of the class (`Kivy` or `KivyMD` widget) that will be added
+ to the menu header.
+
+ .. versionadded:: 0.104.2
+
+ See Header_ for more information.
+
+ :attr:`header_cls` is a :class:`~kivy.properties.ObjectProperty`
+ and defaults to `None`.
+ """
+
+ items = ListProperty()
+ """
+ List of dictionaries with properties for menu items.
+
+ :attr:`items` is a :class:`~kivy.properties.ListProperty`
+ and defaults to `[]`.
+ """
+
+ width_mult = NumericProperty(1, deprecated=True)
+ """
+ This number multiplied by the standard increment ('56dp' on mobile, '64dp'
+ on desktop), determines the width of the menu items.
+
+ If the resulting number were to be too big for the application Window,
+ the multiplier will be adjusted for the biggest possible one.
+
+ .. deprecated:: 1.2.0
+
+ Use `width` instead.
+
+ .. code-block:: python
+
+ self.menu = MDDropdownMenu(
+ width=dp(240),
+ ...,
+ )
+
+ :attr:`width_mult` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `1`.
+ """
+
+ min_height = NumericProperty(dp(48))
+
+ max_height = NumericProperty()
+ """
+ The menu will grow no bigger than this number. Set to 0 for no limit.
:attr:`max_height` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0`.
@@ -622,16 +944,6 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout):
"""
Margin between Window border and menu.
- .. code-block:: python
-
- self.menu = MDDropdownMenu(
- border_margin=dp(4),
- ...,
- )
-
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-border-margin-4.png
- :align: center
-
.. code-block:: python
self.menu = MDDropdownMenu(
@@ -658,7 +970,7 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout):
...,
)
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-ver-growth-up.gif
+ .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-ver-growth-up.png
:align: center
.. code-block:: python
@@ -668,7 +980,7 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout):
...,
)
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-ver-growth-down.gif
+ .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-ver-growth-down.png
:align: center
:attr:`ver_growth` is a :class:`~kivy.properties.OptionProperty`
@@ -687,7 +999,7 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout):
...,
)
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-hor-growth-left.gif
+ .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-hor-growth-left.png
:align: center
.. code-block:: python
@@ -697,48 +1009,25 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout):
...,
)
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-hor-growth-right.gif
+ .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-hor-growth-right.png
:align: center
:attr:`hor_growth` is a :class:`~kivy.properties.OptionProperty`
and defaults to `None`.
"""
- background_color = ColorProperty(None)
+ background_color = ColorProperty(None, deprecated=True)
"""
- Color of the background of the menu.
+ Color in (r, g, b, a) or string format of the background of the menu.
- .. code-block:: python
+ .. deprecated:: 1.2.0
- self.menu = MDDropdownMenu(
- background_color=self.theme_cls.primary_light,
- ...,
- )
-
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-background-color.png
- :align: center
+ Use `md_bg_color` instead.
:attr:`background_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
- opening_transition = StringProperty("out_cubic")
- """
- Type of animation for opening a menu window.
-
- :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty`
- and defaults to `'out_cubic'`.
- """
-
- opening_time = NumericProperty(0.2)
- """
- Menu window opening animation time and you can set it to 0
- if you don't want animation of menu opening.
-
- :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
- and defaults to `0.2`.
- """
-
caller = ObjectProperty()
"""
The widget object that calls the menu window.
@@ -752,13 +1041,10 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout):
)
"""
Menu window position relative to parent element.
- Available options are: `'auto'`, `'center'`, `'bottom'`.
+ Available options are: `'auto'`, `'top'`, `'center'`, `'bottom'`.
See Position_ for more information.
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position.png
- :align: center
-
:attr:`position` is a :class:`~kivy.properties.OptionProperty`
and defaults to `'auto'`.
"""
@@ -767,282 +1053,333 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout):
"""
Menu radius.
- .. code-block:: python
-
- self.menu = MDDropdownMenu(
- radius=[24, 0, 24, 0],
- ...,
- )
-
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-radius.png
- :align: center
-
:attr:`radius` is a :class:`~kivy.properties.VariableListProperty`
and defaults to `'[dp(7)]'`.
"""
- elevation = NumericProperty(4)
+ elevation = NumericProperty(m_res.DROP_DOWN_MENU_ELEVATION)
"""
- Elevation value of menu dialog.
+ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.elevation`
+ attribute.
- .. versionadded:: 1.0.0
+ :attr:`elevation` is an :class:`~kivy.properties.NumericProperty`
+ and defaults to `2`.
+ """
- .. code-block:: python
+ shadow_radius = VariableListProperty([6], length=4)
+ """
+ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_radius`
+ attribute.
- self.menu = MDDropdownMenu(
- elevation=4,
- ...,
- )
+ :attr:`shadow_radius` is an :class:`~kivy.properties.VariableListProperty`
+ and defaults to `[6]`.
+ """
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-elevation.png
- :align: center
+ shadow_softness = NumericProperty(m_res.DROP_DOWN_MENU_SOFTNESS)
+ """
+ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_softness`
+ attribute.
- :attr:`elevation` is an :class:`~kivy.properties.NumericProperty`
- and defaults to `4`.
+ :attr:`shadow_softness` is an :class:`~kivy.properties.NumericProperty`
+ and defaults to `6`.
+ """
+
+ shadow_offset = ListProperty(m_res.DROP_DOWN_MENU_OFFSET)
"""
+ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_offset`
+ attribute.
+ :attr:`shadow_offset` is an :class:`~kivy.properties.ListProperty`
+ and defaults to `(0, -2)`.
+ """
+
+ _items = []
_start_coords = []
- _calculate_complete = False
- _calculate_process = False
+ _tar_x = 0
+ _tar_y = 0
def __init__(self, **kwargs):
super().__init__(**kwargs)
- Window.bind(on_resize=self.check_position_caller)
- Window.bind(on_maximize=self.set_menu_properties)
- Window.bind(on_restore=self.set_menu_properties)
- Clock.schedule_once(self.ajust_radius)
+ Window.bind(
+ on_resize=self._remove_menu,
+ on_maximize=self._remove_menu,
+ on_restore=self._remove_menu,
+ )
self.register_event_type("on_dismiss")
self.menu = self.ids.md_menu
self.target_height = 0
- def check_position_caller(
- self, instance_window: WindowSDL, width: int, height: int
- ) -> None:
- """Called when the application root window is resized."""
+ def adjust_width(self) -> None:
+ """
+ Adjust the width of the menu if the width of the menu goes beyond
+ the boundaries of the parent window from starting point.
+ """
- # FIXME: Menu position is not recalculated when changing the size of
- # the root application window.
- self.set_menu_properties(0)
+ if self._start_coords[0] >= Window.width / 2:
+ if self.width > self._start_coords[0]:
+ self.width = (
+ self._start_coords[0]
+ - self.border_margin
+ - (
+ (self.caller.width / 2 + self.border_margin)
+ if self.position in ["right", "left"]
+ else 0
+ )
+ )
+ else:
+ if Window.width - self._start_coords[0] < self.width:
+ self.width = (
+ Window.width - self._start_coords[0] - self.border_margin
+ )
- def set_menu_properties(self, interval: Union[int, float] = 0) -> None:
- """Sets the size and position for the menu window."""
+ def check_ver_growth(self) -> None:
+ """
+ Checks whether the height of the lower/upper borders of the menu
+ exceeds the limits borders of the parent window.
+ """
- if self.caller:
- self.ids.md_menu.data = self.items
- # We need to pick a starting point, see how big we need to be,
- # and where to grow to.
- self._start_coords = self.caller.to_window(
- self.caller.center_x, self.caller.center_y
- )
- self.target_width = self.width_mult * m_res.STANDARD_INCREMENT
-
- # If we're wider than the Window...
- if self.target_width > Window.width:
- # ...reduce our multiplier to max allowed.
- self.target_width = (
- int(Window.width / m_res.STANDARD_INCREMENT)
- * m_res.STANDARD_INCREMENT
- )
+ if self.target_height > self._start_coords[1] - self.border_margin:
+ self.ver_growth = "up"
+ else:
+ if self._start_coords[1] > Window.height - self._start_coords[1]:
+ self.ver_growth = "down"
- # Set the target_height of the menu depending on the size of
- # each MDMenuItem or MDMenuItemIcon.
- self.target_height = 0
- for item in self.ids.md_menu.data:
- self.target_height += item.get("height", dp(72))
-
- # If we're over max_height...
- if 0 < self.max_height < self.target_height:
- self.target_height = self.max_height
-
- # Establish vertical growth direction.
- if self.ver_growth is not None:
- ver_growth = self.ver_growth
- else:
- # If there's enough space below us:
- if (
- self.target_height
- <= self._start_coords[1] - self.border_margin
- ):
- ver_growth = "down"
- # if there's enough space above us:
- elif (
- self.target_height
- < Window.height - self._start_coords[1] - self.border_margin
- ):
- ver_growth = "up"
- # Otherwise, let's pick the one with more space and adjust
- # ourselves.
- else:
- # If there"s more space below us:
- if (
- self._start_coords[1]
- >= Window.height - self._start_coords[1]
- ):
- ver_growth = "down"
- self.target_height = (
- self._start_coords[1] - self.border_margin
- )
- # If there's more space above us:
- else:
- ver_growth = "up"
- self.target_height = (
- Window.height
- - self._start_coords[1]
- - self.border_margin
- )
-
- if self.hor_growth is not None:
- hor_growth = self.hor_growth
- else:
- # If there's enough space to the right:
- if (
- self.target_width
- <= Window.width - self._start_coords[0] - self.border_margin
- ):
- hor_growth = "right"
- # if there's enough space to the left:
- elif (
- self.target_width
- < self._start_coords[0] - self.border_margin
- ):
- hor_growth = "left"
- # Otherwise, let's pick the one with more space and adjust
- # ourselves.
- else:
- # if there"s more space to the right:
- if (
- Window.width - self._start_coords[0]
- >= self._start_coords[0]
- ):
- hor_growth = "right"
- self.target_width = (
- Window.width
- - self._start_coords[0]
- - self.border_margin
- )
- # if there"s more space to the left:
- else:
- hor_growth = "left"
- self.target_width = (
- self._start_coords[0] - self.border_margin
- )
-
- if ver_growth == "down":
- self.tar_y = self._start_coords[1] - self.target_height
- else: # should always be "up"
- self.tar_y = self._start_coords[1]
-
- if hor_growth == "right":
- self.tar_x = self._start_coords[0]
- else: # should always be "left"
- self.tar_x = self._start_coords[0] - self.target_width
- self._calculate_complete = True
-
- def ajust_radius(self, interval: Union[int, float]) -> None:
+ def check_hor_growth(self) -> None:
"""
- Adjusts the radius of the first and last items in the menu list
- according to the radius that is set for the menu.
+ Checks whether the width of the left/right menu borders exceeds the
+ boundaries of the parent window.
"""
- if self.items:
- radius_for_firt_item = self.radius[:2]
- radius_for_last_item = self.radius[2:]
+ if (
+ Window.width - (self._start_coords[0] + self.border_margin)
+ <= self.width
+ ):
+ self.hor_growth = "left"
+ elif self.width >= self._start_coords[0] + self.border_margin:
+ self.hor_growth = "right"
+
+ def get_target_pos(self) -> [float, float]:
+ self._tar_x, self._tar_y = self._start_coords
- firt_data_item = self.items[0]
- last_data_item = self.items[-1]
+ if self.ver_growth == "up":
+ self._tar_y = self._start_coords[1] + self.height
+ else:
+ self._tar_y = self._start_coords[1]
- firt_data_item["radius"] = radius_for_firt_item + [0, 0]
- last_data_item["radius"] = [0, 0] + radius_for_last_item
- last_data_item["divider"] = None
+ if self.hor_growth == "left":
+ self._tar_x = self._start_coords[0] - self.width
+ else:
+ self._tar_x = self._start_coords[0]
- self.items[0] = firt_data_item
- self.items[-1] = last_data_item
+ return self._tar_x, self._tar_y
- # For all other elements of the list, except for the first and
- # last, we set the value of the radius to `0`.
- for i, data_item in enumerate(self.items):
- if "radius" not in data_item:
- data_item["radius"] = 0
- self.items[i] = data_item
+ def set_target_height(self) -> None:
+ """
+ Set the target height of the menu depending on the size of each item.
+ """
+
+ self.target_height = 0
+ for item in self.menu.data:
+ self.target_height += item.get("height", self.min_height)
+
+ if 0 < self.max_height < self.target_height:
+ self.target_height = self.max_height
+
+ if self._start_coords[1] >= Window.height / 2:
+ if self.target_height > self._start_coords[1]:
+ self.target_height = (
+ self._start_coords[1]
+ - self.border_margin
+ - (
+ (self.caller.height / 2 + self.border_margin)
+ if self.position in ["top", "bottom"]
+ else 0
+ )
+ )
+ else:
+ if Window.height - self._start_coords[1] < self.target_height:
+ self.target_height = (
+ Window.height - self._start_coords[1] - self.border_margin
+ )
+
+ def set_menu_properties(self, *args) -> None:
+ """Sets the size and position for the menu window."""
+
+ if self.caller:
+ self.menu.data = self._items
+ # We need to pick a starting point, see how big we need to be,
+ # and where to grow to.
+ self._start_coords = self.caller.to_window(*self.caller.center)
+
+ self.adjust_width()
+ self.set_target_height()
+ self.check_ver_growth()
+ self.check_hor_growth()
+
+ def set_menu_pos(self, *args) -> None:
+ if self.position == "auto":
+ self.menu.x = self._tar_x
+ self.menu.y = self._tar_y - (
+ self.header_cls.height if self.header_cls else 0
+ )
+ else:
+ if self.position == "center":
+ self.pos = (
+ self._start_coords[0] - self.width / 2,
+ self._start_coords[1] - self.height / 2,
+ )
+ elif self.position == "bottom":
+ self.pos = (
+ (self._start_coords[0] - self.width / 2)
+ if not self.hor_growth
+ else (
+ (self._start_coords[0] - self.width)
+ if self.hor_growth == "left"
+ else (self._start_coords[0])
+ ),
+ self._start_coords[1]
+ - (
+ self.height
+ + self.border_margin
+ + self.caller.height / 2
+ ),
+ )
+ elif self.position == "top":
+ self.pos = (
+ (self._start_coords[0] - self.width / 2)
+ if not self.hor_growth
+ else (
+ (self._start_coords[0] - self.width)
+ if self.hor_growth == "left"
+ else (self._start_coords[0])
+ ),
+ self._start_coords[1]
+ + self.caller.height / 2
+ + self.border_margin,
+ )
def adjust_position(self) -> str:
"""
- Returns value 'auto' for the menu position if the menu position is out
+ Return value 'auto' for the menu position if the menu position is out
of screen.
"""
- target_width = self.target_width
- target_height = self.target_height
- caller = self.caller
position = self.position
- if (
- caller.x < target_width
- or caller.x + target_width > Window.width
- or caller.y + target_height > Window.height
- or (caller.y < target_height and position == "center")
- ):
- position = "auto"
- if self.hor_growth or self.ver_growth:
- self.hor_growth = None
- self.ver_growth = None
- self.set_menu_properties()
+ if position == "bottom":
+ if (
+ self._start_coords[1]
+ - (self.height + self.border_margin + self.caller.height / 2)
+ < 0
+ ):
+ position = "auto"
+ elif position == "top":
+ if (
+ self._start_coords[1]
+ + self.caller.height / 2
+ + self.border_margin
+ > Window.height
+ ):
+ position = "auto"
+ elif position == "center":
+ if (
+ (
+ self._start_coords[1] + self.height / 2 > Window.height
+ or self._start_coords[1] - self.height / 2 < 0
+ )
+ or Window.width - (self._start_coords[0] + self.border_margin)
+ < self.width / 2
+ or self._start_coords[0] + self.border_margin < self.width / 2
+ ):
+ position = "auto"
+
return position
def open(self) -> None:
"""Animate the opening of a menu window."""
- def open(interval):
- if not self._calculate_complete:
- return
-
- position = self.adjust_position()
-
- if position == "auto":
- self.menu.pos = self._start_coords
- anim = Animation(
- x=self.tar_x,
- y=self.tar_y
- - (self.header_cls.height if self.header_cls else 0),
- width=self.target_width,
- height=self.target_height,
- duration=self.opening_time,
- opacity=1,
- transition=self.opening_transition,
- )
- anim.start(self.menu)
- else:
- if position == "center":
- self.menu.pos = (
- self._start_coords[0] - self.target_width / 2,
- self._start_coords[1] - self.target_height / 2,
- )
- elif position == "bottom":
- self.menu.pos = (
- self._start_coords[0] - self.target_width / 2,
- self.caller.pos[1] - self.target_height,
- )
- elif position == "top":
- self.menu.pos = (
- self._start_coords[0] - self.target_width / 2,
- self.caller.pos[1] + self.caller.height,
- )
- anim = Animation(
- width=self.target_width,
- height=self.target_height,
- duration=self.opening_time,
- opacity=1,
- transition=self.opening_transition,
- )
- anim.start(self.menu)
- Window.add_widget(self)
- Clock.unschedule(open)
- self._calculate_process = False
-
self.set_menu_properties()
- if not self._calculate_process:
- self._calculate_process = True
- Clock.schedule_interval(open, 0)
+ Window.add_widget(self)
+ self.position = self.adjust_position()
+
+ if self.width <= 100:
+ self.width = dp(240)
+
+ self.height = self.target_height
+ self._tar_x, self._tar_y = self.get_target_pos()
+ self.x = self._tar_x
+ self.y = self._tar_y - self.target_height
+ self.scale_value_center = self.caller.center
+ self.set_menu_pos()
+ self.on_open()
+
+ def on_items(self, instance, value: list) -> None:
+ """
+ The method sets the class that will be used to create the menu item.
+ """
+
+ items = []
+ viewclass = "MDDropdownTextItem"
+
+ for data in value:
+ if "viewclass" not in data:
+ if (
+ "leading_icon" not in data
+ and "trailing_icon" not in data
+ and "trailing_text" not in data
+ ):
+ viewclass = "MDDropdownTextItem"
+ elif (
+ "leading_icon" in data
+ and "trailing_icon" not in data
+ and "trailing_text" not in data
+ ):
+ viewclass = "MDDropdownLeadingIconItem"
+ elif (
+ "leading_icon" not in data
+ and "trailing_icon" in data
+ and "trailing_text" not in data
+ ):
+ viewclass = "MDDropdownTrailingIconItem"
+ elif (
+ "leading_icon" not in data
+ and "trailing_icon" in data
+ and "trailing_text" in data
+ ):
+ viewclass = "MDDropdownTrailingIconTextItem"
+ elif (
+ "leading_icon" in data
+ and "trailing_icon" in data
+ and "trailing_text" in data
+ ):
+ viewclass = "MDDropdownLeadingTrailingIconTextItem"
+ elif (
+ "leading_icon" in data
+ and "trailing_icon" in data
+ and "trailing_text" not in data
+ ):
+ viewclass = "MDDropdownLeadingTrailingIconItem"
+ elif (
+ "leading_icon" not in data
+ and "trailing_icon" not in data
+ and "trailing_text" in data
+ ):
+ viewclass = "MDDropdownTrailingTextItem"
+ elif (
+ "leading_icon" in data
+ and "trailing_icon" not in data
+ and "trailing_text" in data
+ ):
+ viewclass = "MDDropdownLeadingIconTrailingTextItem"
+
+ data["viewclass"] = viewclass
+
+ if "height" not in data:
+ data["height"] = dp(48)
+
+ items.append(data)
+
+ self._items = items
def on_header_cls(
self, instance_dropdown_menu, instance_user_menu_header
@@ -1070,19 +1407,15 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout):
super().on_touch_up(touch)
return True
- def on_dismiss(self) -> None:
- """Called when the menu is closed."""
-
- Window.remove_widget(self)
- self.menu.width = 0
- self.menu.height = 0
- self.menu.opacity = 0
-
def dismiss(self, *args) -> None:
"""Closes the menu."""
self.on_dismiss()
+ def _remove_menu(self, *args):
+ Window.remove_widget(self)
+ self.set_scale()
+
if __name__ == "__main__":
# To test the correct menu position.
@@ -1097,14 +1430,7 @@ if __name__ == "__main__":
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = MDScreen()
- menu_items = [
- {
- "viewclass": "OneLineListItem",
- "height": dp(56),
- "text": f"Item {i}",
- }
- for i in range(5)
- ]
+ menu_items = [{"text": f"Item {i}"} for i in range(55)]
self.menu = MDDropdownMenu(items=menu_items, width_mult=4)
def open_menu(self, caller):

diff --git a/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.py b/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.py
index 9ac5823f..ac2eac30 100755
--- a/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.py
+++ b/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.py
@@ -575,8 +575,8 @@ class NavigationDrawerContentError(Exception):
class MDNavigationLayout(MDFloatLayout):
"""
- For more information, see in the :class:`~kivymd.uix.floatlayout.MDFloatLayout`
- class documentation.
+ For more information, see in the
+ :class:`~kivymd.uix.floatlayout.MDFloatLayout` class documentation.
"""
_scrim_color = ObjectProperty(None)
@@ -737,7 +737,7 @@ class MDNavigationDrawerDivider(MDBoxLayout):
color = ColorProperty(None)
"""
- Divider color in ``rgba`` format.
+ Divider color in (r, g, b, a) or string format.
:attr:`color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
@@ -811,7 +811,7 @@ class MDNavigationDrawerHeader(MDBoxLayout):
title_color = ColorProperty(None)
"""
- Title text color.
+ Title text color in (r, g, b, a) or string format.
:attr:`title_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
@@ -851,7 +851,7 @@ class MDNavigationDrawerHeader(MDBoxLayout):
text_color = ColorProperty(None)
"""
- Title text color.
+ Title text color in (r, g, b, a) or string format.
:attr:`text_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
@@ -893,7 +893,9 @@ class MDNavigationDrawerItem(OneLineAvatarIconListItem, FocusBehavior):
Implements an item for the :class:`~MDNavigationDrawer` menu list.
For more information, see in the
- :class:`~kivymd.uix.list.OneLineAvatarIconListItem` class documentation.
+ :class:`~kivymd.uix.list.OneLineAvatarIconListItem` and
+ :class:`~kivymd.uix.behaviors.FocusBehavior`
+ class documentation.
.. versionadded:: 1.0.0
@@ -936,7 +938,7 @@ class MDNavigationDrawerItem(OneLineAvatarIconListItem, FocusBehavior):
icon_color = ColorProperty(None)
"""
- Icon color item.
+ Icon color in (r, g, b, a) or string format item.
:attr:`icon_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
@@ -944,7 +946,8 @@ class MDNavigationDrawerItem(OneLineAvatarIconListItem, FocusBehavior):
selected_color = ColorProperty([0, 0, 0, 1])
"""
- The color of the icon and text of the selected item.
+ The color in (r, g, b, a) or string format of the icon and text of the
+ selected item.
:attr:`selected_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `[0, 0, 0, 1]`.
@@ -960,7 +963,7 @@ class MDNavigationDrawerItem(OneLineAvatarIconListItem, FocusBehavior):
text_right_color = ColorProperty(None)
"""
- Right text color item.
+ Right text color item in (r, g, b, a) or string format.
:attr:`text_right_color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
@@ -1095,9 +1098,9 @@ class MDNavigationDrawer(MDCard):
# FIXME: Doesn't work in Kivy v2.1.0.
scrim_color = ColorProperty([0, 0, 0, 0.5])
"""
- Color for scrim. Alpha channel will be multiplied with
- :attr:`_scrim_alpha`. Set fourth channel to 0 if you want to disable
- scrim.
+ Color for scrim in (r, g, b, a) or string format. Alpha channel will be
+ multiplied with :attr:`_scrim_alpha`. Set fourth channel to 0 if you want
+ to disable scrim.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-scrim-color.png
:align: center

diff --git a/sbapp/kivymd/uix/navigationrail/navigationrail.py b/sbapp/kivymd/uix/navigationrail/navigationrail.py
index 42e2e247..762c74d6 100644
--- a/sbapp/kivymd/uix/navigationrail/navigationrail.py
+++ b/sbapp/kivymd/uix/navigationrail/navigationrail.py
@@ -8,26 +8,14 @@ Components/NavigationRail
`Material Design spec, Navigation rail <https://m3.material.io/components/navigation-rail/specs>`_
-.. rubric::
-
- Navigation rails provide access to primary destinations in apps when using
- tablet and desktop screens.
+.. rubric:: Navigation rails provide access to primary destinations in apps
+ when using tablet and desktop screens.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail.png
:align: center
Usage
-=====
-
-.. code-block:: kv
-
- MDNavigationRail:
-
- MDNavigationRailItem:
-
- MDNavigationRailItem:
-
- MDNavigationRailItem:
+-----
.. tabs::
@@ -113,6 +101,21 @@ Usage
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-usage.png
:align: center
+Anatomy
+-------
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-anatomy.png
+ :align: center
+
+1. Container
+2. Label text (optional)
+3. Icon
+4. Active indicator
+5. Badge (optional)
+6. Large badge (optional)
+7. Large badge label (optional)
+8. Menu icon (optional)
+
Example
=======
@@ -137,9 +140,8 @@ Example
<ExtendedButton>
- elevation: 3.5
+ elevation: 1
shadow_radius: 12
- shadow_softness: 4
-height: "56dp"
@@ -207,9 +209,9 @@ Example
MDNavigationDrawer:
id: nav_drawer
- radius: (0, 16, 16, 0)
+ radius: 0, 16, 16, 0
md_bg_color: "#fffcf4"
- elevation: 4
+ elevation: 2
width: "240dp"
MDNavigationDrawerMenu:
@@ -218,14 +220,18 @@ Example
orientation: "vertical"
adaptive_height: True
spacing: "12dp"
- padding: "3dp", 0, 0, "12dp"
+ padding: 0, 0, 0, "12dp"
MDIconButton:
icon: "menu"
- ExtendedButton:
- text: "Compose"
- icon: "pencil"
+ MDBoxLayout:
+ adaptive_height: True
+ padding: "12dp", 0, 0, 0
+
+ ExtendedButton:
+ text: "Compose"
+ icon: "pencil"
DrawerClickableItem:
text: "Python"
@@ -269,7 +275,9 @@ Example
def set_radius(self, *args):
if self.rounded_button:
- self._radius = self.radius = self.height / 4
+ value = self.height / 4
+ self.radius = [value, value, value, value]
+ self._radius = value
class Example(MDApp):
@@ -357,9 +365,8 @@ Example
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.padding = "16dp"
- self.elevation = 3.5
+ self.elevation = 1
self.shadow_radius = 12
- self.shadow_softness = 4
self.height = dp(56)
Clock.schedule_once(self.set_spacing)
@@ -439,9 +446,13 @@ Example
MDIconButton(
icon="menu",
),
- ExtendedButton(
- text="Compose",
- icon="pencil",
+ MDBoxLayout(
+ ExtendedButton(
+ text="Compose",
+ icon="pencil",
+ ),
+ adaptive_height=True,
+ padding=["12dp", 0, 0, 0],
),
orientation="vertical",
adaptive_height=True,
@@ -540,6 +551,7 @@ from typing import Union
from kivy.animation import Animation
from kivy.clock import Clock
+from kivy.core.window import Window
from kivy.lang import Builder
from kivy.logger import Logger
from kivy.metrics import dp
@@ -556,7 +568,6 @@ from kivy.properties import (
from kivy.uix.behaviors import ButtonBehavior
from kivymd import uix_path
-from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import ScaleBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import MDFloatingActionButton, MDIconButton
@@ -591,7 +602,12 @@ class RippleWidget(MDWidget, ScaleBehavior):
class MDNavigationRailFabButton(MDFloatingActionButton):
- """Implements an optional floating action button (FAB)."""
+ """
+ Implements an optional floating action button (FAB).
+
+ For more information, see in the
+ :class:`~kivymd.uix.button.MDFloatingActionButton` class documentation.
+ """
icon = StringProperty("pencil")
"""
@@ -613,7 +629,12 @@ class MDNavigationRailFabButton(MDFloatingActionButton):
class MDNavigationRailMenuButton(MDIconButton):
- """Implements a menu button."""
+ """
+ Implements a menu button.
+
+ For more information, see in the
+ :class:`~kivymd.uix.button.MDIconButton` classes documentation.
+ """
icon = StringProperty("menu")
"""
@@ -634,8 +655,15 @@ class MDNavigationRailMenuButton(MDIconButton):
"""
-class MDNavigationRailItem(ThemableBehavior, ButtonBehavior, MDBoxLayout):
- """Implements a menu item with an icon and text."""
+class MDNavigationRailItem(ButtonBehavior, MDBoxLayout):
+ """
+ Implements a menu item with an icon and text.
+
+ For more information, see in the
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout`
+ classes documentation.
+ """
navigation_rail = ObjectProperty()
"""
@@ -814,6 +842,11 @@ class MDNavigationRailItem(ThemableBehavior, ButtonBehavior, MDBoxLayout):
class MDNavigationRail(MDCard):
"""
+ Navigation rail class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.card.MDCard` class documentation.
+
:Events:
:attr:`on_item_press`
Called on the `on_press` event of menu item -
@@ -941,7 +974,8 @@ class MDNavigationRail(MDCard):
text_color_item_normal = ColorProperty(None)
"""
- The text color of the normal menu item (:class:`~MDNavigationRailItem`).
+ The text color in (r, g, b, a) or string format of the normal menu item
+ (:class:`~MDNavigationRailItem`).
.. code-block:: kv
@@ -960,7 +994,8 @@ class MDNavigationRail(MDCard):
text_color_item_active = ColorProperty(None)
"""
- The text color of the active menu item (:class:`~MDNavigationRailItem`).
+ The text color in (r, g, b, a) or string format of the active menu item
+ (:class:`~MDNavigationRailItem`).
.. code-block:: kv
@@ -979,7 +1014,8 @@ class MDNavigationRail(MDCard):
icon_color_item_normal = ColorProperty(None)
"""
- The icon color of the normal menu item (:class:`~MDNavigationRailItem`).
+ The icon color in (r, g, b, a) or string format of the normal menu item
+ (:class:`~MDNavigationRailItem`).
.. code-block:: kv
@@ -998,7 +1034,8 @@ class MDNavigationRail(MDCard):
icon_color_item_active = ColorProperty(None)
"""
- The icon color of the active menu item (:class:`~MDNavigationRailItem`).
+ The icon color in (r, g, b, a) or string format of the active menu item
+ (:class:`~MDNavigationRailItem`).
.. code-block:: kv
@@ -1110,6 +1147,9 @@ class MDNavigationRail(MDCard):
self.register_event_type("on_item_press")
self.register_event_type("on_item_release")
+ def on_size(self, *args):
+ Clock.schedule_once(self.set_pos_menu_fab_buttons)
+
def on_item_press(self, *args) -> None:
"""
Called on the `on_press` event of menu item -
@@ -1188,7 +1228,7 @@ class MDNavigationRail(MDCard):
items[index].dispatch("on_press")
items[index].dispatch("on_release")
- def set_pos_menu_fab_buttons(self, interval: Union[int, float]) -> None:
+ def set_pos_menu_fab_buttons(self, *args) -> None:
"""
Sets the position of the :class:`~MDNavigationRailFabButton` and
:class:`~MDNavigationRailMenuButton` buttons on the panel.

diff --git a/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.py b/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.py
index 37a0e52d..43727023 100644
--- a/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.py
+++ b/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.py
@@ -100,7 +100,6 @@ from PIL import ImageDraw
from kivymd import uix_path
from kivymd.color_definitions import colors as _colors
from kivymd.color_definitions import text_colors
-from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import RectangularRippleBehavior
from kivymd.uix.behaviors.toggle_behavior import MDToggleButton
from kivymd.uix.boxlayout import MDBoxLayout
@@ -123,9 +122,11 @@ class TypeColorButton(MDRaisedButton, MDToggleButton):
'RGBA', 'HEX', 'RGB'.
"""
- theme_text_color = "Custom"
- text_color = (0, 0, 0, 1)
- elevation = 0
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.theme_text_color = "Custom"
+ self.text_color = (0, 0, 0, 1)
+ self.elevation = 0
class SelectAlphaChannelWidget(MDBoxLayout):
@@ -186,7 +187,7 @@ class SliderTab(MDBoxLayout):
"""Basic event handler for changing the slider value."""
-class GradientTab(ThemableBehavior, MDBoxLayout):
+class GradientTab(MDBoxLayout):
"""
The class implements a tab with a gradient, a color selection scale and
a scale for setting the transparency value of the selected color.
@@ -398,8 +399,8 @@ class MDColorPicker(BaseDialog):
default_color = ColorProperty(None, allownone=True)
"""
- Default color value The set color value will be used when you open the
- dialog.
+ Default color value in (r, g, b, a) or string format. The set color value
+ will be used when you open the dialog.
:attr:`default_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
@@ -416,7 +417,8 @@ class MDColorPicker(BaseDialog):
background_down_button_selected_type_color = ColorProperty([1, 1, 1, 0.3])
"""
- Button background for choosing a color type ('RGBA', 'HEX', 'HSL', 'RGB').
+ Button background for choosing a color type ('RGBA', 'HEX', 'HSL', 'RGB')
+ in (r, g, b, a) or string format.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/color-picker-background-down-button-selected-type-color.png
:align: center

diff --git a/sbapp/kivymd/uix/pickers/datepicker/datepicker.kv b/sbapp/kivymd/uix/pickers/datepicker/datepicker.kv
index 13835dee..7fe28667 100644
--- a/sbapp/kivymd/uix/pickers/datepicker/datepicker.kv
+++ b/sbapp/kivymd/uix/pickers/datepicker/datepicker.kv
@@ -31,9 +31,7 @@
canvas:
Color:
- rgb:
- app.theme_cls.primary_color \
- if not root.primary_color else root.primary_color
+ rgb: root.primary_color or app.theme_cls.primary_color
RoundedRectangle:
size:
(dp(328), dp(120)) \
@@ -48,9 +46,7 @@
if root.theme_cls.device_orientation == "portrait" \
else (root.radius[0], dp(0), dp(0), root.radius[3])
Color:
- rgba:
- app.theme_cls.bg_normal \
- if not root.accent_color else root.accent_color
+ rgba: root.accent_color or app.theme_cls.bg_normal
RoundedRectangle:
size:
(dp(328), dp(512) - dp(120) - root._shift_dialog_height) \
@@ -79,9 +75,7 @@
(dp(24), root.height - self.height - dp(18)) \
if root.theme_cls.device_orientation == "portrait" \
else (dp(24), root.height - self.height - dp(24))
- text_color:
- root.specific_text_color \
- if not root.text_toolbar_color else root.text_toolbar_color
+ text_color: root.text_toolbar_color or root.specific_text_color
MDLabel:
id: label_full_date
@@ -100,28 +94,13 @@
dp(24) if not root._input_date_dialog_open else dp(168) + dp(24), \
root.height - self.height - dp(96) \
)
- text:
- root.set_text_full_date(root.sel_year, root.sel_month, root.sel_day, \
- root.theme_cls.device_orientation)
+ text: root._date_label_text
text_color:
- ( \
- root.specific_text_color \
- if not root.text_toolbar_color else root.text_toolbar_color \
- ) \
- if root.theme_cls.device_orientation == "portrait" \
- else \
- ( \
- ( \
- self.theme_cls.primary_color \
- if not root.primary_color else root.primary_color \
- ) \
- if root._input_date_dialog_open \
- else \
- ( \
- root.specific_text_color \
- if not root.text_toolbar_color else root.text_toolbar_color \
- ) \
- )
+ root.text_toolbar_color or root.specific_text_color \
+ if root.theme_cls.device_orientation == "portrait" else \
+ root.primary_color or self.theme_cls.primary_color \
+ if root._input_date_dialog_open else \
+ root.text_toolbar_color or root.specific_text_color
RecycleView:
id: _year_layout
@@ -164,9 +143,7 @@
(root.height - dp(120) + dp(12)) \
if root.theme_cls.device_orientation == "portrait" \
else dp(12)
- text_color:
- root.specific_text_color \
- if not root.text_toolbar_color else root.text_toolbar_color
+ text_color: root.text_toolbar_color or root.specific_text_color
MDLabel:
id: label_month_selector
@@ -180,9 +157,7 @@
(dp(24), root.height - dp(120) - self.height - dp(20)) \
if root.theme_cls.device_orientation == "portrait" \
else (dp(168) + dp(24), label_title.y)
- text_color:
- app.theme_cls.text_color \
- if not root.text_color else root.text_color
+ text_color: root.text_color or app.theme_cls.text_color
DatePickerIconTooltipButton:
id: triangle
@@ -199,9 +174,7 @@
(label_month_selector.width + dp(14), root.height - dp(123) - self.height) \
if root.theme_cls.device_orientation == "portrait" \
else (dp(180) + label_month_selector.width, label_title.y - dp(14))
- text_color:
- app.theme_cls.text_color \
- if not root.text_color else root.text_color
+ text_color: root.text_color or app.theme_cls.text_color
md_bg_color_disabled: 0, 0, 0, 0
DatePickerIconTooltipButton:
@@ -218,9 +191,7 @@
root.height - dp(120) - self.height / 2 - dp(30) \
if root.theme_cls.device_orientation == "portrait" \
else dp(272)
- text_color:
- app.theme_cls.text_color \
- if not root.text_color else root.text_color
+ text_color: root.text_color or app.theme_cls.text_color
DatePickerIconTooltipButton:
id: chevron_right
@@ -236,9 +207,7 @@
root.height - dp(120) - self.height / 2 - dp(30) \
if root.theme_cls.device_orientation == "portrait" \
else dp(272)
- text_color:
- app.theme_cls.text_color \
- if not root.text_color else root.text_color
+ text_color: root.text_color or app.theme_cls.text_color
# TODO: Replace the GridLayout with a RecycleView
# if it improves performance.
@@ -280,10 +249,7 @@
text: "OK"
theme_text_color: "Custom"
font_name: root.font_name
- text_color:
- root.theme_cls.primary_color \
- if not root.text_button_color else \
- root.text_button_color
+ text_color: root.text_button_color or root.theme_cls.primary_color
on_release: root.on_ok_button_pressed()
MDFlatButton:
@@ -293,10 +259,7 @@
theme_text_color: "Custom"
pos: root.width - self.width - ok_button.width - dp(10), dp(10)
font_name: root.font_name
- text_color:
- root.theme_cls.primary_color \
- if not root.text_button_color else \
- root.text_button_color
+ text_color: root.text_button_color or root.theme_cls.primary_color
<DatePickerDaySelectableItem>
@@ -312,14 +275,8 @@
canvas.before:
Color:
rgba:
- (\
- self.owner.selector_color[:-1] + [.3] \
- if self.owner.selector_color \
- else self.theme_cls.primary_color[:-1] + [.3] \
- ) \
- if not self.disabled \
- and self.text \
- and self.check_date(self.owner.year, self.owner.month, int(self.text)) \
+ (self.owner.selector_color or self.theme_cls.primary_color)[:-1] + [.3] \
+ if self.is_in_range \
else (0, 0, 0, 0)
RoundedRectangle:
size:
@@ -327,55 +284,24 @@
if root.theme_cls.device_orientation == "portrait" \
else \
(dp(32), dp(28)) \
- if self.index in [6, 13, 20, 27, 34] or self.owner._date_range \
- and self.text and self.owner._date_range[-1] == date( \
- self.current_year, \
- self.current_month, \
- int(self.text) \
- ) \
- or self.text and int(self.text) == \
- calendar.monthrange(self.current_year, self.current_month)[1] \
+ if self.is_range_end or self.is_week_end or self.is_month_end \
else (dp(46), dp(28))
pos:
(self.x - dp(1.5), self.y + dp(5)) \
if root.theme_cls.device_orientation == "portrait" else \
(self.x, self.y + 1)
radius:
- [0, 0, 0, 0] if not self.owner._date_range else \
- ( \
- [self.width / 2, 0, 0, self.width / 2] \
- if self.text and self.owner._date_range[0] == date( \
- self.current_year, \
- self.current_month, \
- int(self.text) \
- ) \
- or (self.index in [0, 7, 14, 21, 28] and root.is_selected) \
- else \
- ( \
- [0, 0, 0, 0] if self.text \
- and self.owner._date_range[-1] != date( \
- self.current_year, \
- self.current_month, \
- int(self.text) \
- ) \
- and self.index not in [6, 13, 20, 27, 30] \
- else [0, self.width / 2, self.width, 0] \
- if root.is_selected or self.text \
- and self.owner._date_range[-1] == date( \
- self.current_year, \
- self.current_month, \
- int(self.text) \
- ) \
- else [0, 0, 0, 0]) \
- )
+ [
+ self.width / 2 if self.is_range_start else 0,
+ self.width / 2 if self.is_range_end else 0,
+ self.width / 2 if self.is_range_end else 0,
+ self.width / 2 if self.is_range_start else 0,
+ ]
# Selection circle.
Color:
rgba:
- ( \
- self.theme_cls.primary_color if not root.owner.selector_color \
- else root.owner.selector_color \
- ) \
+ root.owner.selector_color or self.theme_cls.primary_color \
if root.is_selected and not self.disabled \
else (0, 0, 0, 0)
Ellipse:
@@ -393,25 +319,11 @@
font_name: root.owner.font_name
theme_text_color: "Custom"
text_color:
- ( \
- root.theme_cls.primary_color \
- if not root.owner.text_current_color \
- else root.owner.text_current_color \
- ) \
- if root.is_today and not root.is_selected \
- else ( \
- ( \
- root.theme_cls.text_color \
- if not root.is_selected or root.owner.mode == "range" \
- else (1, 1, 1, 1) \
- ) \
- if not root.owner.text_color \
- else \
- ( \
- root.owner.text_color \
- if not root.is_selected else (1, 1, 1, 1)) \
- )
-
+ root.owner.accent_color or root.theme_cls.bg_normal \
+ if root.is_selected else \
+ root.owner.text_current_color or root.theme_cls.primary_color \
+ if root.is_today else \
+ root.owner.text_color or root.theme_cls.text_color
<DatePickerWeekdayLabel>
font_style: "Caption"
@@ -425,9 +337,7 @@
size:
(dp(40), dp(40)) if root.theme_cls.device_orientation == "portrait" \
else (dp(32), dp(32))
- text_color:
- app.theme_cls.disabled_hint_text_color \
- if not root.owner.text_weekday_color else root.owner.text_weekday_color
+ text_color: root.owner.text_weekday_color or app.theme_cls.disabled_hint_text_color
<DatePickerYearSelectableItem>
@@ -436,13 +346,21 @@
valign: "middle"
halign: "center"
text: root.text
+ theme_text_color: "Custom"
+ text_color:
+ (0, 0, 0, 0) \
+ if self.owner is None else \
+ self.owner.accent_color or self.owner.theme_cls.bg_normal \
+ if self.selected else \
+ self.owner.text_color or self.owner.theme_cls.text_color
on_text: root.font_name = root.owner.font_name
canvas.before:
Color:
rgba:
- root.selected_color if root.selected_color \
- else self.theme_cls.primary_color
+ self.owner.selector_color or self.theme_cls.primary_color \
+ if self.selected else \
+ (0, 0, 0, 0)
RoundedRectangle:
pos: self.x + dp(12), self.y
size: self.width - dp(24), self.height
@@ -453,6 +371,7 @@
adaptive_height: True
size_hint_x: None
spacing: dp(8)
+ opacity: 0
width:
self.owner.width - dp(48) \
if root.owner.theme_cls.device_orientation == "portrait" \
@@ -468,10 +387,6 @@
<DatePickerInputField>
mode: "fill"
- opacity: 0
hint_text: "dd/mm/yyyy"
input_filter: root.input_filter
- fill_color:
- (0, 0, 0, .15) \
- if not self.owner.input_field_background_color \
- else root.owner.input_field_background_color
+ fill_color: root.owner.input_field_background_color or (0, 0, 0, .15)

diff --git a/sbapp/kivymd/uix/pickers/datepicker/datepicker.py b/sbapp/kivymd/uix/pickers/datepicker/datepicker.py
index 3188408c..3b24f9ed 100644
--- a/sbapp/kivymd/uix/pickers/datepicker/datepicker.py
+++ b/sbapp/kivymd/uix/pickers/datepicker/datepicker.py
@@ -203,7 +203,6 @@ from datetime import date
from itertools import zip_longest
from typing import Union
-from kivy import Logger
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.metrics import dp
@@ -253,6 +252,12 @@ class BaseDialogPicker(
Base class for :class:`~kivymd.uix.picker.MDDatePicker` and
:class:`~kivymd.uix.picker.MDTimePicker` classes.
+ For more information, see in the
+ :class:`~kivymd.uix.dialog.BaseDialog` and
+ :class:`~kivymd.uix.behaviors.CommonElevationBehavior` and
+ :class:`~kivymd.uix.behaviors.SpecificBackgroundColorBehavior`
+ classes documentation.
+
:Events:
`on_save`
Events called when the "OK" dialog box button is clicked.
@@ -644,11 +649,30 @@ class DatePickerTypeDateError(Exception):
class DatePickerInputField(MDTextField):
- """Implements date input in dd/mm/yyyy format."""
+ """
+ Implements date input in dd/mm/yyyy format.
+
+ For more information, see in the
+ :class:`~kivymd.uix.textfield.MDTextField` class documentation.
+ """
helper_text_mode = StringProperty("on_error")
owner = ObjectProperty() # MDDatePicker object
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.bind(text=self._on_text_check_errors)
+
+ def _on_text_check_errors(self, widget, text):
+ if text == "":
+ self.error = False
+ return
+ try:
+ datetime.datetime.strptime(text, "%d/%m/%Y")
+ self.error = False
+ except ValueError:
+ self.error = True
+
def set_error(self):
"""Sets a text field to an error state."""
@@ -699,54 +723,14 @@ class DatePickerDaySelectableItem(
owner = ObjectProperty()
is_today = BooleanProperty(False)
is_selected = BooleanProperty(False)
- current_month = NumericProperty()
- current_year = NumericProperty()
- index = NumericProperty(0)
-
- def check_date(self, year: int, month: int, day: int):
- try:
- return date(year, month, day) in self.owner._date_range
- except ValueError as error:
- if str(error) == "day is out of range for month":
- return False
+ is_in_range = BooleanProperty(False)
+ is_range_start = BooleanProperty(False)
+ is_range_end = BooleanProperty(False)
+ is_month_end = BooleanProperty(False)
+ is_week_end = BooleanProperty(False)
def on_release(self):
- if (
- self.owner.mode == "range"
- and self.owner._end_range_date
- and self.owner._start_range_date
- ):
- return
- if (
- not self.owner._input_date_dialog_open
- and not self.owner._select_year_dialog_open
- ):
- if self.owner.mode == "range" and not self.owner._start_range_date:
- self.owner._start_range_date = date(
- self.current_year, self.current_month, int(self.text)
- )
- self.owner.min_date = self.owner._start_range_date
- elif (
- self.owner.mode == "range"
- and not self.owner._end_range_date
- and self.owner._start_range_date
- ):
- self.owner._end_range_date = date(
- self.current_year, self.current_month, int(self.text)
- )
- if self.owner._end_range_date <= self.owner.min_date:
- toast(self.owner.date_range_text_error)
- Logger.error(
- "`Data Picker: max_date` value cannot be less than "
- "or equal to 'min_date' value."
- )
- self.owner._start_range_date = 0
- self.owner._end_range_date = 0
- return
- self.owner.max_date = self.owner._end_range_date
- self.owner.update_calendar_for_date_range()
-
- self.owner.set_selected_widget(self)
+ self.owner.set_selected_widget(self)
def on_touch_down(self, touch):
# If year_layout is active don't dispatch on_touch_down events,
@@ -760,7 +744,7 @@ class DatePickerYearSelectableItem(RecycleDataViewBehavior, MDLabel):
"""Implements an item for a pick list of the year."""
index = None
- selected_color = ColorProperty([0, 0, 0, 0])
+ selected = BooleanProperty(False)
owner = ObjectProperty()
def refresh_view_attrs(self, rv, index, data):
@@ -772,32 +756,10 @@ class DatePickerYearSelectableItem(RecycleDataViewBehavior, MDLabel):
return True
if self.collide_point(*touch.pos):
self.owner.year = int(self.text)
- # self.owner.sel_year = self.owner.year
- self.owner.ids.label_full_date.text = self.owner.set_text_full_date(
- self.owner.sel_year,
- self.owner.sel_month,
- self.owner.sel_day,
- self.owner.theme_cls.device_orientation,
- )
return self.parent.select_with_touch(self.index, touch)
def apply_selection(self, table_data, index, is_selected):
- if is_selected:
- self.selected_color = (
- self.owner.selector_color
- if self.owner.selector_color
- else self.theme_cls.primary_color
- )
- self.text_color = (1, 1, 1, 1)
- else:
- if int(self.text) == self.owner.sel_year:
- self.text_color = (
- self.theme_cls.primary_color
- if not self.owner.text_current_color
- else self.owner.text_current_color
- )
- self.selected_color = [0, 0, 0, 0]
- self.text_color = (0, 0, 0, 1)
+ self.selected = is_selected
# TODO: Add the feature to embed the `MDDatePicker` class in other layouts
@@ -889,7 +851,7 @@ class MDDatePicker(BaseDialogPicker):
and defaults to `picker`.
"""
- min_date = ObjectProperty()
+ min_date = ObjectProperty(allownone=True)
"""
The minimum value of the date range for the `'mode`' parameter.
Must be an object <class 'datetime.date'>.
@@ -900,7 +862,7 @@ class MDDatePicker(BaseDialogPicker):
and defaults to `None`.
"""
- max_date = ObjectProperty()
+ max_date = ObjectProperty(allownone=True)
"""
The minimum value of the date range for the `'mode`' parameter.
Must be an object <class 'datetime.date'>.
@@ -955,17 +917,13 @@ class MDDatePicker(BaseDialogPicker):
_calendar_layout = ObjectProperty()
_calendar_list = None
- _enter_data_field = None
- _enter_data_field_two = None
- _enter_data_field_container = None
- _date_range = []
+ _fields_container = None
_scale_calendar_layout = NumericProperty(1)
_scale_year_layout = NumericProperty(0)
_shift_dialog_height = NumericProperty(0)
_input_date_dialog_open = BooleanProperty(False)
_select_year_dialog_open = False
- _start_range_date = 0
- _end_range_date = 0
+ _date_label_text = StringProperty()
def __init__(
self,
@@ -996,7 +954,6 @@ class MDDatePicker(BaseDialogPicker):
"'max_date' must be of class <class 'datetime.date'>"
)
self.compare_date_range()
- self._date_range = self.get_date_range()
self.generate_list_widgets_days()
self.update_calendar(self.sel_year, self.sel_month)
@@ -1006,6 +963,8 @@ class MDDatePicker(BaseDialogPicker):
) -> None:
"""Called when the device's screen orientation changes."""
+ # Separators of the label text depend on the orientation.
+ self._update_date_label_text()
if self._input_date_dialog_open:
if orientation_value == "portrait":
self._shift_dialog_height = dp(250)
@@ -1017,21 +976,12 @@ class MDDatePicker(BaseDialogPicker):
Called when the 'OK' button is pressed to confirm the date entered.
"""
- if self._enter_data_field and not self.is_date_valaid(
- self._enter_data_field.text
- ):
- self._enter_data_field.set_error()
- return
- if self._enter_data_field_two and not self.is_date_valaid(
- self._enter_data_field_two.text
- ):
- self._enter_data_field_two.set_error()
+ if self._input_date_dialog_open and not self._try_apply_input():
return
-
self.dispatch(
"on_save",
date(self.sel_year, self.sel_month, self.sel_day),
- self._date_range,
+ self.get_date_range(),
)
def is_date_valaid(self, date: str) -> bool:
@@ -1085,56 +1035,25 @@ class MDDatePicker(BaseDialogPicker):
self.ids._year_layout.children[0].clear_selection()
def transformation_to_dialog_input_date(self) -> None:
- def set_date_to_input_field():
- if not self._enter_data_field_two:
- # Date of current day.
- self._enter_data_field.text = (
- f"{'' if self.sel_day >= 10 else '0'}"
- f"{self.sel_day}/"
- f"{'' if self.sel_month >= 10 else '0'}"
- f"{self.sel_month}/{self.sel_year}"
- )
- else:
- # Range start date.
- self._enter_data_field.text = (
- f"{'' if self.min_date.day >= 10 else '0'}"
- f"{self.min_date.day}/"
- f"{'' if self.min_date.month >= 10 else '0'}"
- f"{self.min_date.month}/{self.min_date.year}"
- )
-
- def set_date_to_input_field_two() -> None:
- # Range end date.
- self._enter_data_field_two.text = (
- f"{'' if self.max_date.day >= 10 else '0'}"
- f"{self.max_date.day}/"
- f"{'' if self.max_date.month >= 10 else '0'}"
- f"{self.max_date.month}/{self.max_date.year}"
- )
-
self.ids.triangle.disabled = True
if self._select_year_dialog_open:
self.transformation_from_dialog_select_year()
self._input_date_dialog_open = True
-
- self._enter_data_field_container = DatePickerInputFieldContainer(
- owner=self
- )
- self._enter_data_field = self.get_field()
- if self.min_date and self.max_date:
- self._enter_data_field_two = self.get_field()
- set_date_to_input_field_two()
- set_date_to_input_field()
- self._enter_data_field_container.add_widget(self._enter_data_field)
- if self._enter_data_field_two:
- self._enter_data_field_container.add_widget(
- self._enter_data_field_two
- )
-
- self.ids.container.add_widget(self._enter_data_field_container)
self.ids.edit_icon.icon = "calendar"
self.ids.label_title.text = self.title_input
+ self._fields_container = DatePickerInputFieldContainer(owner=self)
+ if self.mode == "picker":
+ selected_date = date(self.sel_year, self.sel_month, self.sel_day)
+ selected_dates = [selected_date]
+ else:
+ selected_dates = [self.min_date, self.max_date]
+ for selected_date in selected_dates:
+ field = self.get_field(selected_date)
+ field.bind(text=self._on_date_field_text_changes)
+ self._fields_container.add_widget(field)
+ self.ids.container.add_widget(self._fields_container)
+
Animation(
_shift_dialog_height=dp(250)
if self.theme_cls.device_orientation == "portrait"
@@ -1152,28 +1071,22 @@ class MDDatePicker(BaseDialogPicker):
).start(self.ids.chevron_right)
Animation(opacity=0, d=0.15).start(self.ids.label_month_selector)
Animation(opacity=0, d=0.15).start(self.ids.triangle)
- Animation(opacity=1, d=0.15).start(self._enter_data_field)
- if self._enter_data_field_two:
- Animation(opacity=1, d=0.15).start(self._enter_data_field_two)
- self.ids.label_full_date.text = self.set_text_full_date(
- self.sel_year,
- self.sel_month,
- self.sel_day,
- self.theme_cls.device_orientation,
- )
+ Animation(opacity=1, d=0.15).start(self._fields_container)
+ # The label text separator in landscape orientation depends on the
+ # open dialog.
+ self._update_date_label_text()
def transformation_from_dialog_input_date(
self, interval: Union[int, float]
) -> None:
+ if not self._try_apply_input():
+ return
self._input_date_dialog_open = False
- self.ids.label_full_date.text = self.set_text_full_date(
- self.sel_year,
- self.sel_month,
- self.sel_day,
- self.theme_cls.device_orientation,
- )
self.ids.triangle.disabled = False
- self.ids.container.remove_widget(self._enter_data_field_container)
+ self.ids.edit_icon.icon = "pencil"
+ self.ids.label_title.text = self.title
+ self.ids.container.remove_widget(self._fields_container)
+ self._fields_container = None
Animation(
_shift_dialog_height=dp(0), _scale_calendar_layout=1, d=0.15
).start(self)
@@ -1187,41 +1100,67 @@ class MDDatePicker(BaseDialogPicker):
).start(self.ids.chevron_right)
Animation(opacity=1, d=0.15).start(self.ids.label_month_selector)
Animation(opacity=1, d=0.15).start(self.ids.triangle)
- Animation(opacity=0, d=0.15).start(self._enter_data_field)
- self.ids.edit_icon.icon = "pencil"
- self.ids.label_title.text = self.title
+ # The label text separator in landscape orientation depends on the
+ # open dialog.
+ self._update_date_label_text()
- if not self.min_date and not self.max_date:
- list_date = self._enter_data_field.get_list_date()
- if len(list_date) == 3 and len(list_date[2]) == 4:
- self.sel_day = int(list_date[0])
- self.sel_month = int(list_date[1])
- self.sel_year = int(list_date[2])
- self.update_calendar(self.sel_year, self.sel_month)
- elif self.min_date and self.max_date:
- list_min_date = self._enter_data_field.get_list_date()
- list_max_date = self._enter_data_field_two.get_list_date()
-
- if len(list_min_date) == 3 and len(list_min_date[2]) == 4:
- self.min_date = date(
- int(list_min_date[2]),
- int(list_min_date[1]),
- int(list_min_date[0]),
- )
- if len(list_max_date) == 3 and len(list_max_date[2]) == 4:
- self.max_date = date(
- int(list_max_date[2]),
- int(list_max_date[1]),
- int(list_max_date[0]),
- )
+ def _get_dates_from_fields(self):
+ """
+ Return a list of dates entered by the user in the input fields.
- self.update_calendar_for_date_range()
- self.ids.label_full_date.text = self.set_text_full_date(
- int(list_max_date[2]),
- int(list_max_date[1]),
- int(list_max_date[0]),
- self.theme_cls.device_orientation,
- )
+ If there is an error in the field or the field is empty, None will be
+ in its place in the list. The length of the list will be 0 if the input
+ dialog is closed, otherwise 1 in picker mode or 2 in range mode.
+ """
+
+ if not self._fields_container:
+ return []
+
+ dates = []
+ # Widgets are arranged in the reverse order of their addition.
+ for field in reversed(self._fields_container.children):
+ try:
+ date = datetime.datetime.strptime(field.text, "%d/%m/%Y").date()
+ except ValueError:
+ date = None
+ dates.append(date)
+
+ return dates
+
+ def _try_apply_input(self) -> bool:
+ """
+ Apply the dates entered by the user, update the calendar and return
+ True. If there are errors in the fields, do nothing and return False.
+ """
+
+ dates = self._get_dates_from_fields()
+ if not dates:
+ return True
+
+ # Widgets are arranged in the reverse order of their addition.
+ fields = reversed(self._fields_container.children)
+ if any(d is None and f.text for f, d in zip(fields, dates)):
+ return False
+
+ if self.mode == "picker":
+ selected_date = date(self.sel_year, self.sel_month, self.sel_day)
+ selected_date = dates[0] or selected_date
+ self.sel_year = selected_date.year
+ self.sel_month = selected_date.month
+ self.sel_day = selected_date.day
+ self.update_calendar(self.sel_year, self.sel_month)
+ elif self.mode == "range":
+ date1, date2 = dates[0] or self.min_date, dates[1] or self.max_date
+ ends = list(filter(bool, [date1, date2]))
+ if ends:
+ self.min_date = min(ends)
+ self.max_date = max(ends)
+ self.update_calendar(self.year, self.month)
+
+ return True
+
+ def _on_date_field_text_changes(self, *args):
+ self._update_date_label_text()
def compare_date_range(self) -> None:
# TODO: Add behavior if the minimum date range exceeds the maximum
@@ -1233,8 +1172,7 @@ class MDDatePicker(BaseDialogPicker):
)
def update_calendar_for_date_range(self) -> None:
- # self.compare_date_range()
- self._date_range = self.get_date_range()
+ # This method is no longer used, use update_calendar instead.
self.update_calendar(self.year, self.month)
def update_text_full_date(self, list_date) -> None:
@@ -1243,27 +1181,13 @@ class MDDatePicker(BaseDialogPicker):
in an open date input dialog.
"""
- if len(list_date) == 1 and len(list_date[0]) == 2:
- self.ids.label_full_date.text = self.set_text_full_date(
- self.sel_year,
- self.sel_month,
- list_date[0],
- self.theme_cls.device_orientation,
- )
- if len(list_date) == 2 and len(list_date[1]) == 2:
- self.ids.label_full_date.text = self.set_text_full_date(
- self.sel_year,
- int(list_date[1]),
- int(list_date[0]),
- self.theme_cls.device_orientation,
- )
- if len(list_date) == 3 and len(list_date[2]) == 4:
- self.ids.label_full_date.text = self.set_text_full_date(
- int(list_date[2]),
- int(list_date[1]),
- int(list_date[0]),
- self.theme_cls.device_orientation,
- )
+ # This method no longer used, use update_calendar instead.
+ year = int(list_date[2]) if len(list_date) > 2 else self.sel_year
+ month = int(list_date[1]) if len(list_date) > 1 else self.sel_month
+ day = int(list_date[0]) if len(list_date) > 0 else self.sel_day
+ day = min(day, calendar.monthrange(year, month)[1])
+ self.sel_year, self.sel_month, self.sel_day = year, month, day
+ self.update_calendar(year, month)
def update_calendar(self, year, month) -> None:
self.year, self.month = year, month
@@ -1271,7 +1195,10 @@ class MDDatePicker(BaseDialogPicker):
selected_date = date(self.sel_year, self.sel_month, self.sel_day)
selected_dates = {selected_date}
else:
- selected_dates = {self._start_range_date, self._end_range_date}
+ selected_dates = {self.min_date, self.max_date}
+ # The label text depends on the selected date or date range.
+ self._update_date_label_text()
+ month_end = date(year, month, calendar.monthrange(year, month)[1])
dates = self.calendar.itermonthdates(year, month)
for widget, widget_date in zip_longest(self._calendar_list, dates):
# Only widgets whose dates are in the displayed month are visible.
@@ -1281,21 +1208,31 @@ class MDDatePicker(BaseDialogPicker):
and widget_date.year == year
)
widget.text = str(widget_date.day) if visible else ""
- widget.current_year = year
- widget.current_month = month
widget.is_today = visible and widget_date == self.today
widget.is_selected = visible and widget_date in selected_dates
# I don't understand why, but this line is important. Without this
# line, some widgets that we are trying to disable remain enabled.
widget.disabled = False
- widget.disabled = (
- not visible
- or self.mode == "range"
- and self._date_range
- and widget_date not in self._date_range
+ widget.disabled = not visible
+ widget.is_in_range = (
+ visible
+ and self.min_date is not None
+ and self.max_date is not None
+ and self.min_date <= widget_date <= self.max_date
+ )
+ widget.is_range_start = (
+ visible
+ and self.min_date is not None
+ and widget_date == self.min_date
)
+ widget.is_range_end = (
+ visible
+ and self.max_date is not None
+ and widget_date == self.max_date
+ )
+ widget.is_month_end = widget_date == month_end
- def get_field(self) -> MDTextField:
+ def get_field(self, date=None) -> MDTextField:
"""Creates and returns a text field object used to enter dates."""
if issubclass(self.input_field_cls, MDTextField):
@@ -1322,6 +1259,7 @@ class MDDatePicker(BaseDialogPicker):
field = self.input_field_cls(
owner=self,
+ text=date.strftime("%d/%m/%Y") if date else "",
helper_text=self.helper_text,
fill_color_normal=fill_color_normal,
fill_color_focus=fill_color_focus,
@@ -1340,6 +1278,8 @@ class MDDatePicker(BaseDialogPicker):
)
def get_date_range(self) -> list:
+ if not self.min_date or not self.max_date:
+ return []
date_range = [
self.min_date + datetime.timedelta(days=x)
for x in range((self.max_date - self.min_date).days + 1)
@@ -1353,118 +1293,73 @@ class MDDatePicker(BaseDialogPicker):
a date range.
"""
- if 12 < int(month) < 0:
- raise ValueError(
- "set_text_full_date:\n\t" f"Month [{month}] out of range."
- )
- if int(day) > calendar.monthrange(int(year), (month))[1]:
- return ""
- date = datetime.date(int(year), int(month), int(day))
- separator = (
- "\n"
- if (orientation == "landscape" and not self._input_date_dialog_open)
- else " "
- )
+ # In portrait orientation, the label is stretched in width, so we
+ # should not insert line breaks. When the input dialog is open, the
+ # label moves to the right and also stretches in width.
+ horizontal = orientation == "portrait" or self._input_date_dialog_open
+
+ def date_repr(date):
+ return date.strftime("%b").capitalize() + " " + str(date.day)
+ input_dates = self._get_dates_from_fields()
if self.mode == "picker":
- if not self.min_date and not self.max_date:
- return (
- date.strftime("%a,").capitalize()
- + separator
- + date.strftime("%b ").capitalize()
- + str(day).lstrip("0")
- )
- else:
- return (
- self.min_date.strftime("%b ").capitalize()
- + str(self.min_date.day).lstrip("0")
- + (
- " - "
- if orientation == "portrait"
- else (
- ",\n" if not self._input_date_dialog_open else ", "
- )
- )
- + self.max_date.strftime("%b ").capitalize()
- + str(self.max_date.day).lstrip("0")
- )
+ selected_date = date(self.sel_year, self.sel_month, self.sel_day)
+ if input_dates:
+ selected_date = input_dates[0] or selected_date
+ weekday_repr = selected_date.strftime("%a").capitalize()
+ separator = ", " if horizontal else ",\n"
+ return weekday_repr + separator + date_repr(selected_date)
elif self.mode == "range":
- if self._start_range_date and self._end_range_date:
- if (
- orientation == "landscape"
- and "-" in self.ids.label_full_date.text
- ):
- return (
- self.ids.label_full_date.text.split("-")[0].strip()
- + (",\n" if not self._input_date_dialog_open else " - ")
- + date.strftime("%b ").capitalize()
- + str(day).lstrip("0")
- )
- else:
- if (
- orientation == "landscape"
- and "," in self.ids.label_full_date.text
- ):
- return (
- self.ids.label_full_date.text.split(",")[0].strip()
- + (
- ",\n"
- if not self._input_date_dialog_open
- else "-"
- )
- + date.strftime("%b ").capitalize()
- + str(day).lstrip("0")
- )
- if (
- orientation == "portrait"
- and "," in self.ids.label_full_date.text
- ):
- return (
- self.ids.label_full_date.text.split(",")[0].strip()
- + "-"
- + date.strftime("%b ").capitalize()
- + str(day).lstrip("0")
- )
- if (
- orientation == "portrait"
- and "-" in self.ids.label_full_date.text
- ):
- return (
- self.ids.label_full_date.text.split("-")[0].strip()
- + " - "
- + date.strftime("%b ").capitalize()
- + str(day).lstrip("0")
- )
- elif self._start_range_date and not self._end_range_date:
- return (
- (
- date.strftime("%b ").capitalize()
- + str(day).lstrip("0")
- + " - End"
- )
- if orientation != "landscape"
- else (
- date.strftime("%b ").capitalize()
- + str(day).lstrip("0")
- + "{}End".format(
- ",\n" if not self._input_date_dialog_open else " - "
- )
- )
- )
- elif not self._start_range_date and not self._end_range_date:
- return (
- "Start - End"
- if orientation != "landscape"
- else "Start{}End".format(
- ",\n" if not self._input_date_dialog_open else " - "
- )
- )
+ start, end = self.min_date, self.max_date
+ if input_dates:
+ start, end = input_dates[0] or start, input_dates[1] or end
+ ends = [end for end in (start, end) if end]
+ if len(ends) == 0:
+ start_repr, end_repr = "Start", "End"
+ else:
+ start, end = min(ends), max(ends)
+ start_repr, end_repr = date_repr(start), date_repr(end)
+ separator = " — " if horizontal else ",\n"
+ return start_repr + separator + end_repr
+
+ def _update_date_label_text(self):
+ self._date_label_text = self.set_text_full_date(
+ self.sel_year,
+ self.sel_month,
+ self.sel_day,
+ self.theme_cls.device_orientation,
+ )
def set_selected_widget(self, widget) -> None:
- self.sel_year = self.year
- self.sel_month = self.month
- self.sel_day = int(widget.text)
- self.update_calendar(self.sel_year, self.sel_month)
+ if self._select_year_dialog_open or self._input_date_dialog_open:
+ return
+ try:
+ widget_date = date(self.year, self.month, int(widget.text))
+ except ValueError:
+ return
+ if self.mode == "picker":
+ self.sel_year = widget_date.year
+ self.sel_month = widget_date.month
+ self.sel_day = widget_date.day
+ self.update_calendar(self.sel_year, self.sel_month)
+ elif self.mode == "range":
+ ends = [end for end in (self.min_date, self.max_date) if end]
+ if widget_date in ends:
+ ends = [end for end in ends if end != widget_date]
+ elif len(ends) < 2:
+ ends.append(widget_date)
+ else:
+ start, end = min(ends), max(ends)
+ if abs(widget_date - start).days < abs(widget_date - end).days:
+ start = widget_date
+ else:
+ end = widget_date
+ ends = [start, end]
+ if len(ends) == 0:
+ self.min_date, self.max_date = None, None
+ else:
+ self.min_date, self.max_date = min(ends), max(ends)
+ self.update_calendar(self.year, self.month)
def set_month_day(self, day) -> None:
# This method is no longer used. The code bellow repeats the behavior
@@ -1525,12 +1420,10 @@ class MDDatePicker(BaseDialogPicker):
)
weekday_label.font_name = self.font_name
self._calendar_layout.add_widget(weekday_label)
- for i, j in enumerate(range(6 * 7)): # 6 weeks, 7 days a week
+ for i in range(6 * 7): # 6 weeks, 7 days a week
day_selectable_item = DatePickerDaySelectableItem(
- index=i,
+ is_week_end=i % 7 == 6,
owner=self,
- current_month=int(self.month),
- current_year=int(self.year),
)
calendar_list.append(day_selectable_item)
self._calendar_layout.add_widget(day_selectable_item)
@@ -1541,9 +1434,11 @@ class MDDatePicker(BaseDialogPicker):
Called when "chevron-left" and "chevron-right" buttons are pressed.
Switches the calendar to the previous/next month.
"""
+
month_delta = 1 if operation == "next" else -1
year = self.year + (self.month - 1 + month_delta) // 12
month = (self.month - 1 + month_delta) % 12 + 1
+
if year <= 0:
year, month = 1, 1
self.update_calendar(year, month)

diff --git a/sbapp/kivymd/uix/pickers/timepicker/timepicker.py b/sbapp/kivymd/uix/pickers/timepicker/timepicker.py
index 2b646d80..e45a7841 100644
--- a/sbapp/kivymd/uix/pickers/timepicker/timepicker.py
+++ b/sbapp/kivymd/uix/pickers/timepicker/timepicker.py
@@ -166,7 +166,6 @@ from kivy.uix.behaviors import ButtonBehavior
from kivy.vector import Vector
from kivymd import uix_path
-from kivymd.theming import ThemableBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.circularlayout import MDCircularLayout
from kivymd.uix.label import MDLabel
@@ -185,7 +184,7 @@ class AmPmSelectorLabel(ButtonBehavior, MDLabel):
pass
-class AmPmSelector(ThemableBehavior, MDBoxLayout):
+class AmPmSelector(MDBoxLayout):
border_radius = NumericProperty()
border_color = ColorProperty()
bg_color = ColorProperty()

diff --git a/sbapp/kivymd/uix/progressbar/progressbar.kv b/sbapp/kivymd/uix/progressbar/progressbar.kv
index 00028cfb..89aafb89 100644
--- a/sbapp/kivymd/uix/progressbar/progressbar.kv
+++ b/sbapp/kivymd/uix/progressbar/progressbar.kv
@@ -6,7 +6,8 @@
self.theme_cls.divider_color \
if not self.back_color else \
self.back_color
- Rectangle:
+ RoundedRectangle:
+ radius: root.radius
size:
(self.width, self.height) \
if self.orientation == "horizontal" else \
@@ -18,7 +19,8 @@
Color:
rgba:
self.theme_cls.primary_color if not self.color else self.color
- Rectangle:
+ RoundedRectangle:
+ radius: root.radius
size:
(self.width * self.value_normalized, self.height if self.height else dp(4)) \
if self.orientation == "horizontal" else \

diff --git a/sbapp/kivymd/uix/progressbar/progressbar.py b/sbapp/kivymd/uix/progressbar/progressbar.py
index 9cedee86..b785f03a 100644
--- a/sbapp/kivymd/uix/progressbar/progressbar.py
+++ b/sbapp/kivymd/uix/progressbar/progressbar.py
@@ -145,6 +145,7 @@ from kivy.properties import (
NumericProperty,
OptionProperty,
StringProperty,
+ VariableListProperty,
)
from kivy.uix.progressbar import ProgressBar
@@ -158,6 +159,25 @@ with open(
class MDProgressBar(ThemableBehavior, ProgressBar):
+ """
+ Progressbar class.
+
+ For more information, see in the
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivy.uix.progressbar.ProgressBar`
+ classes documentation.
+ """
+
+ radius = VariableListProperty([0], length=4)
+ """
+ Progress line radius.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`radius` is an :class:`~kivy.properties.VariableListProperty`
+ and defaults to `[0, 0, 0, 0]`.
+ """
+
reversed = BooleanProperty(False)
"""
Reverse the direction the progressbar moves.
@@ -179,7 +199,7 @@ class MDProgressBar(ThemableBehavior, ProgressBar):
color = ColorProperty(None)
"""
- Progress bar color in ``rgba`` format.
+ Progress bar color in (r, g, b, a) or string format.
:attr:`color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
@@ -187,7 +207,7 @@ class MDProgressBar(ThemableBehavior, ProgressBar):
back_color = ColorProperty(None)
"""
- Progress bar back color in ``rgba`` format.
+ Progress bar back color in (r, g, b, a) or string format.
.. versionadded:: 1.0.0

diff --git a/sbapp/kivymd/uix/recyclegridlayout.py b/sbapp/kivymd/uix/recyclegridlayout.py
index d3ed891f..e519af53 100644
--- a/sbapp/kivymd/uix/recyclegridlayout.py
+++ b/sbapp/kivymd/uix/recyclegridlayout.py
@@ -85,12 +85,13 @@ Equivalent
from kivy.uix.recyclegridlayout import RecycleGridLayout
+from kivymd.theming import ThemableBehavior
from kivymd.uix import MDAdaptiveWidget
from kivymd.uix.behaviors import DeclarativeBehavior
class MDRecycleGridLayout(
- DeclarativeBehavior, RecycleGridLayout, MDAdaptiveWidget
+ DeclarativeBehavior, ThemableBehavior, RecycleGridLayout, MDAdaptiveWidget
):
"""
Recycle grid layout layout class. For more information, see in the

diff --git a/sbapp/kivymd/uix/recycleview.py b/sbapp/kivymd/uix/recycleview.py
index 97921034..ddcf16c1 100644
--- a/sbapp/kivymd/uix/recycleview.py
+++ b/sbapp/kivymd/uix/recycleview.py
@@ -34,10 +34,14 @@ __all__ = ("MDRecycleView",)
from kivy.uix.recycleview import RecycleView
+from kivymd.theming import ThemableBehavior
+from kivymd.uix import MDAdaptiveWidget
from kivymd.uix.behaviors import DeclarativeBehavior
-class MDRecycleView(DeclarativeBehavior, RecycleView):
+class MDRecycleView(
+ DeclarativeBehavior, ThemableBehavior, RecycleView, MDAdaptiveWidget
+):
"""
Recycle view class. For more information, see in the
:class:`~kivy.uix.recycleview.RecycleView` class documentation.

diff --git a/sbapp/kivymd/uix/refreshlayout/refreshlayout.kv b/sbapp/kivymd/uix/refreshlayout/refreshlayout.kv
index 26dc4889..e58b7f67 100644
--- a/sbapp/kivymd/uix/refreshlayout/refreshlayout.kv
+++ b/sbapp/kivymd/uix/refreshlayout/refreshlayout.kv
@@ -15,7 +15,7 @@
canvas:
Clear
Color:
- rgba: root.theme_cls.primary_dark
+ rgba: root.circle_color
Ellipse:
pos: self.pos
size: self.size
@@ -24,4 +24,4 @@
id: spinner
size_hint: None, None
size: dp(30), dp(30)
- color: 1, 1, 1, 1
+ color: root.spinner_color

diff --git a/sbapp/kivymd/uix/refreshlayout/refreshlayout.py b/sbapp/kivymd/uix/refreshlayout/refreshlayout.py
index 170b76e3..c11672d6 100755
--- a/sbapp/kivymd/uix/refreshlayout/refreshlayout.py
+++ b/sbapp/kivymd/uix/refreshlayout/refreshlayout.py
@@ -43,6 +43,8 @@ Example
id: refresh_layout
refresh_callback: app.refresh_callback
root_layout: root
+ spinner_color: "brown"
+ circle_color: "white"
MDGridLayout:
id: box
@@ -66,6 +68,8 @@ Example
y = 15
def build(self):
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
self.screen = Factory.Example()
self.set_list()
@@ -81,8 +85,10 @@ Example
asynckivy.start(set_list())
def refresh_callback(self, *args):
- '''A method that updates the state of your application
- while the spinner remains on the screen.'''
+ '''
+ A method that updates the state of your application
+ while the spinner remains on the screen.
+ '''
def refresh_callback(interval):
self.screen.ids.box.clear_widgets()
@@ -110,7 +116,12 @@ from kivy.core.window import Window
from kivy.effects.dampedscroll import DampedScrollEffect
from kivy.lang import Builder
from kivy.metrics import dp
-from kivy.properties import ColorProperty, NumericProperty, ObjectProperty
+from kivy.properties import (
+ ColorProperty,
+ NumericProperty,
+ ObjectProperty,
+ StringProperty,
+)
from kivy.uix.floatlayout import FloatLayout
from kivymd import uix_path
@@ -150,7 +161,16 @@ class _RefreshScrollEffect(DampedScrollEffect):
return False
-class MDScrollViewRefreshLayout(MDScrollView):
+class MDScrollViewRefreshLayout(ThemableBehavior, MDScrollView):
+ """
+ Refresh layout class.
+
+ For more information, see in the
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivymd.uix.scrollview.MDScrollView`
+ class documentation.
+ """
+
root_layout = ObjectProperty()
"""
The spinner will be attached to this layout.
@@ -168,8 +188,70 @@ class MDScrollViewRefreshLayout(MDScrollView):
and defaults to `None`.
"""
+ spinner_color = ColorProperty([1, 1, 1, 1])
+ """
+ Color of the spinner in (r, g, b, a) or string format.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`spinner_color` is a :class:`~kivy.properties.ColorProperty`
+ and defaults to `[1, 1, 1, 1]`.
+ """
+
+ circle_color = ColorProperty(None)
+ """
+ Color of the ellipse around the spinner in (r, g, b, a) or string format.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`circle_color` is a :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
+ """
+
+ show_transition = StringProperty("out_elastic")
+ """
+ Transition of the spinner's opening.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`show_transition` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `'out_elastic'`.
+ """
+
+ show_duration = NumericProperty(0.8)
+ """
+ Duration of the spinner display.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`show_duration` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `0.8`.
+ """
+
+ hide_transition = StringProperty("out_elastic")
+ """
+ Transition of hiding the spinner.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`hide_transition` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `'out_elastic'`.
+ """
+
+ hide_duration = NumericProperty(0.8)
+ """
+ Duration of hiding the spinner.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`hide_duration` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `0.8`.
+ """
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+ if not self.circle_color:
+ self.circle_color = self.theme_cls.primary_dark
self.effect_cls = _RefreshScrollEffect
self._work_spinner = False
self._did_overscroll = False
@@ -180,7 +262,15 @@ class MDScrollViewRefreshLayout(MDScrollView):
if self.refresh_callback:
self.refresh_callback()
if not self.refresh_spinner:
- self.refresh_spinner = RefreshSpinner(_refresh_layout=self)
+ self.refresh_spinner = RefreshSpinner(
+ _refresh_layout=self,
+ spinner_color=self.spinner_color,
+ circle_color=self.circle_color,
+ show_transition=self.show_transition,
+ show_duration=self.show_duration,
+ hide_transition=self.hide_transition,
+ hide_duration=self.hide_duration,
+ )
self.root_layout.add_widget(self.refresh_spinner)
self.refresh_spinner.start_anim_spinner()
self._work_spinner = True
@@ -195,13 +285,18 @@ class MDScrollViewRefreshLayout(MDScrollView):
class RefreshSpinner(ThemableBehavior, FloatLayout):
+ # Color of the spinner in (r, g, b, a) or string format.
spinner_color = ColorProperty([1, 1, 1, 1])
- """
- Color of spinner.
-
- :attr:`spinner_color` is a :class:`~kivy.properties.ColorProperty`
- and defaults to `[1, 1, 1, 1]`.
- """
+ # Color of the ellipse around the spinner in (r, g, b, a) or string format.
+ circle_color = ColorProperty()
+ # Transition of the spinner's opening.
+ show_transition = StringProperty()
+ # The duration of the spinner display.
+ show_duration = NumericProperty(0.8)
+ # Transition of hiding the spinner.
+ hide_transition = StringProperty()
+ # Duration of hiding the spinner.
+ hide_duration = NumericProperty(0.8)
# kivymd.refreshlayout.MDScrollViewRefreshLayout object
_refresh_layout = ObjectProperty()
@@ -210,13 +305,15 @@ class RefreshSpinner(ThemableBehavior, FloatLayout):
spinner = self.ids.body_spinner
Animation(
y=spinner.y - self.theme_cls.standard_increment * 2 + dp(10),
- d=0.8,
- t="out_elastic",
+ d=self.show_duration,
+ t=self.show_transition,
).start(spinner)
def hide_anim_spinner(self) -> None:
spinner = self.ids.body_spinner
- anim = Animation(y=Window.height, d=0.8, t="out_elastic")
+ anim = Animation(
+ y=Window.height, d=self.hide_duration, t=self.hide_transition
+ )
anim.bind(on_complete=self.set_spinner)
anim.start(spinner)

diff --git a/sbapp/kivymd/uix/relativelayout.py b/sbapp/kivymd/uix/relativelayout.py
index 64f4658f..b0e58e1f 100644
--- a/sbapp/kivymd/uix/relativelayout.py
+++ b/sbapp/kivymd/uix/relativelayout.py
@@ -31,11 +31,14 @@ MDRelativeLayout
from kivy.uix.relativelayout import RelativeLayout
+from kivymd.theming import ThemableBehavior
from kivymd.uix import MDAdaptiveWidget
from kivymd.uix.behaviors import DeclarativeBehavior
-class MDRelativeLayout(DeclarativeBehavior, RelativeLayout, MDAdaptiveWidget):
+class MDRelativeLayout(
+ DeclarativeBehavior, ThemableBehavior, RelativeLayout, MDAdaptiveWidget
+):
"""
Relative layout class. For more information, see in the
:class:`~kivy.uix.relativelayout.RelativeLayout` class documentation.

diff --git a/sbapp/kivymd/uix/screen.py b/sbapp/kivymd/uix/screen.py
index 2e399ecf..1f24676c 100644
--- a/sbapp/kivymd/uix/screen.py
+++ b/sbapp/kivymd/uix/screen.py
@@ -32,12 +32,13 @@ MDScreen
from kivy.properties import ListProperty, ObjectProperty
from kivy.uix.screenmanager import Screen
+from kivymd.theming import ThemableBehavior
from kivymd.uix import MDAdaptiveWidget
from kivymd.uix.behaviors import DeclarativeBehavior
from kivymd.uix.hero import MDHeroTo
-class MDScreen(DeclarativeBehavior, Screen, MDAdaptiveWidget):
+class MDScreen(DeclarativeBehavior, ThemableBehavior, Screen, MDAdaptiveWidget):
"""
Screen is an element intended to be used with a
:class:`~kivymd.uix.screenmanager.MDScreenManager`. For more information,

diff --git a/sbapp/kivymd/uix/segmentedbutton/__init__.py b/sbapp/kivymd/uix/segmentedbutton/__init__.py
new file mode 100644
index 00000000..f516e1c7
--- /dev/null
+++ b/sbapp/kivymd/uix/segmentedbutton/__init__.py
@@ -0,0 +1,4 @@
+from .segmentedbutton import ( # NOQA F401
+ MDSegmentedButton,
+ MDSegmentedButtonItem,
+)

diff --git a/sbapp/kivymd/uix/segmentedbutton/segmentedbutton.kv b/sbapp/kivymd/uix/segmentedbutton/segmentedbutton.kv
new file mode 100644
index 00000000..c4b63ffc
--- /dev/null
+++ b/sbapp/kivymd/uix/segmentedbutton/segmentedbutton.kv
@@ -0,0 +1,32 @@
+<MDSegmentedButton>
+ size_hint: None, None
+ height: "40dp"
+ opacity: 0
+
+
+<MDSegmentedButtonItem>
+ size_hint: None, None
+ height: self.parent.height
+ line_color:
+ self.theme_cls.disabled_hint_text_color \
+ if self.parent.line_color == [0, 0, 0, 0] else \
+ self.parent.line_color
+
+ SegmentButtonIcon:
+ id: scale_icon
+ icon: root.icon
+ size_hint: None, None
+ size: "24dp", "24dp"
+ pos_hint: {"center_y": .5}
+ scale_value_x: 1 if root.icon else 0
+ scale_value_y: 1 if root.icon else 0
+ x: label_text.x - dp(32)
+
+ MDLabel:
+ id: label_text
+ text: root.text
+ adaptive_size: True
+ pos_hint: {"center_y": .5}
+ x:
+ root.center_x - (self.texture_size[0] / 2) \
+ + (dp(16) if root.icon else 0)

diff --git a/sbapp/kivymd/uix/segmentedbutton/segmentedbutton.py b/sbapp/kivymd/uix/segmentedbutton/segmentedbutton.py
new file mode 100644
index 00000000..0729dba1
--- /dev/null
+++ b/sbapp/kivymd/uix/segmentedbutton/segmentedbutton.py
@@ -0,0 +1,653 @@
+"""
+Components/SegmentedButton
+==========================
+
+.. versionadded:: 1.2.0
+
+.. seealso::
+
+ `Material Design spec, Segmented buttons <https://m3.material.io/components/segmented-buttons/overview>`_
+
+ `Segmented control <https://kivymd.readthedocs.io/en/latest/components/segmentedcontrol/>`_
+
+.. rubric:: Segmented buttons help people select options, switch views,
+ or sort elements.
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-preview.png
+ :align: center
+
+Usage
+-----
+
+.. code-block:: kv
+
+ MDScreen:
+
+ MDSegmentedButton:
+
+ MDSegmentedButtonItem:
+ icon: ...
+ text: ...
+
+ MDSegmentedButtonItem:
+ icon: ...
+ text: ...
+
+ MDSegmentedButtonItem:
+ icon: ...
+ text: ...
+
+Example
+-------
+
+.. code-block:: python
+
+ from kivy.lang import Builder
+
+ from kivymd.app import MDApp
+
+ KV = '''
+ MDScreen:
+
+ MDSegmentedButton:
+ pos_hint: {"center_x": .5, "center_y": .5}
+
+ MDSegmentedButtonItem:
+ text: "Walking"
+
+ MDSegmentedButtonItem:
+ text: "Transit"
+
+ MDSegmentedButtonItem:
+ text: "Driving"
+ '''
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ return Builder.load_string(KV)
+
+
+ Example().run()
+
+By default, segmented buttons support single marking of elements:
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-multiselect-false.gif
+ :align: center
+
+For multiple marking of elements, use the
+:attr:`kivymd.uix.segmentedbutton.segmentedbutton.MDSegmentedButton.multiselect`
+parameter:
+
+.. code-block:: kv
+
+ MDSegmentedButton:
+ multiselect: True
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-multiselect-true.gif
+ :align: center
+
+Control width
+-------------
+
+The width of the panel of segmented buttons will be equal to the width
+of the texture of the widest button multiplied by the number of buttons:
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-width-by-default.png
+ :align: center
+
+But you can use the `size_hint_x` parameter to specify the relative width:
+
+.. code-block:: kv
+
+ MDSegmentedButton:
+ size_hint_x: .9
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-width-size-hint-x.png
+ :align: center
+
+Customization
+-------------
+
+You can see below in the documentation from which classes the
+:class:`~kivymd.uix.segmentedbutton.segmentedbutton.MDSegmentedButton` and
+:class:`~kivymd.uix.segmentedbutton.segmentedbutton.MDSegmentedButtonItem`
+classes are inherited and use all their attributes such as
+`md_bg_color`, `md_bg_color` etc. for additional customization of segments.
+
+Events
+------
+
+- on_marked
+ The method is called when a segment is marked.
+
+- on_unmarked
+ The method is called when a segment is unmarked.
+
+.. code-block:: kv
+
+ MDSegmentedButton:
+ on_marked: app.on_marked(*args)
+
+.. code-block:: python
+
+ def on_marked(
+ self,
+ segment_button: MDSegmentedButton,
+ segment_item: MDSegmentedButtonItem,
+ marked: bool,
+ ) -> None:
+ print(segment_button)
+ print(segment_item)
+ print(marked)
+
+A practical example
+-------------------
+
+.. code-block:: python
+
+ import os
+
+ from faker import Faker
+
+ from kivy.clock import Clock
+ from kivy.lang import Builder
+ from kivy.properties import StringProperty
+
+ from kivymd.app import MDApp
+ from kivymd.uix.boxlayout import MDBoxLayout
+ from kivymd.uix.segmentedbutton import MDSegmentedButton, MDSegmentedButtonItem
+ from kivymd.utils import asynckivy
+
+ KV = '''
+ <UserCard>
+ adaptive_height: True
+ md_bg_color: "#343930"
+ radius: 16
+
+ TwoLineAvatarListItem:
+ id: item
+ divider: None
+ _no_ripple_effect: True
+ text: root.name
+ secondary_text: root.path_to_file
+ theme_text_color: "Custom"
+ text_color: "#8A8D79"
+ secondary_theme_text_color: self.theme_text_color
+ secondary_text_color: self.text_color
+ on_size:
+ self.ids._left_container.size = (item.height, item.height)
+ self.ids._left_container.x = dp(6)
+ self._txt_right_pad = item.height + dp(12)
+
+ ImageLeftWidget:
+ source: root.album
+ radius: root.radius
+
+
+ MDScreen:
+ md_bg_color: "#151514"
+
+ MDBoxLayout:
+ orientation: "vertical"
+ padding: "12dp"
+ spacing: "12dp"
+
+ MDLabel:
+ adaptive_height: True
+ text: "Your downloads"
+ font_style: "H5"
+ theme_text_color: "Custom"
+ text_color: "#8A8D79"
+
+ MDSegmentedButton:
+ size_hint_x: 1
+ selected_color: "#303A29"
+ line_color: "#343930"
+ on_marked: app.on_marked(*args)
+
+ MDSegmentedButtonItem:
+ text: "Songs"
+ active: True
+
+ MDSegmentedButtonItem:
+ text: "Albums"
+
+ MDSegmentedButtonItem:
+ text: "Podcasts"
+
+ RecycleView:
+ id: card_list
+ viewclass: "UserCard"
+ bar_width: 0
+
+ RecycleBoxLayout:
+ orientation: 'vertical'
+ spacing: "16dp"
+ padding: "16dp"
+ default_size: None, dp(72)
+ default_size_hint: 1, None
+ size_hint_y: None
+ height: self.minimum_height
+ '''
+
+
+ class UserCard(MDBoxLayout):
+ name = StringProperty()
+ path_to_file = StringProperty()
+ album = StringProperty()
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ return Builder.load_string(KV)
+
+ def on_marked(
+ self,
+ segment_button: MDSegmentedButton,
+ segment_item: MDSegmentedButtonItem,
+ marked: bool,
+ ) -> None:
+ self.generate_card()
+
+ def generate_card(self):
+ async def generate_card():
+ for i in range(10):
+ await asynckivy.sleep(0)
+ self.root.ids.card_list.data.append(
+ {
+ "name": fake.name(),
+ "path_to_file": f"{os.path.splitext(fake.file_path())[0]}.mp3",
+ "album": fake.image_url(),
+ }
+ )
+
+ fake = Faker()
+ self.root.ids.card_list.data = []
+ Clock.schedule_once(lambda x: asynckivy.start(generate_card()))
+
+
+ Example().run()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-practical-example.gif
+ :align: center
+"""
+
+from __future__ import annotations
+
+__all__ = ("MDSegmentedButton", "MDSegmentedButtonItem")
+
+import os
+
+from kivy.animation import Animation
+from kivy.clock import Clock
+from kivy.lang import Builder
+from kivy.metrics import dp
+from kivy.properties import (
+ BooleanProperty,
+ ColorProperty,
+ ListProperty,
+ NumericProperty,
+ StringProperty,
+ VariableListProperty,
+)
+from kivy.uix.behaviors import ButtonBehavior
+
+from kivymd import uix_path
+from kivymd.uix.behaviors import RectangularRippleBehavior, ScaleBehavior
+from kivymd.uix.boxlayout import MDBoxLayout
+from kivymd.uix.floatlayout import MDFloatLayout
+from kivymd.uix.label import MDIcon
+
+with open(
+ os.path.join(uix_path, "segmentedbutton", "segmentedbutton.kv"),
+ encoding="utf-8",
+) as kv_file:
+ Builder.load_string(kv_file.read())
+
+
+class MDSegmentedButtonItem(
+ RectangularRippleBehavior, ButtonBehavior, MDFloatLayout
+):
+ """
+ Segment button item.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and
+ :class:`~kivy.uix.behaviors.ButtonBehavior` and
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout`
+ class documentation.
+ """
+
+ icon = StringProperty()
+ """
+ Icon segment.
+
+ :attr:`icon` is an :class:`~kivy.properties.StringProperty`
+ and defaults to `''`.
+ """
+
+ text = StringProperty()
+ """
+ Text segment.
+
+ :attr:`text` is an :class:`~kivy.properties.StringProperty`
+ and defaults to `''`.
+ """
+
+ active = BooleanProperty(False)
+ """
+ Background color of an disabled segment.
+
+ :attr:`active` is an :class:`~kivy.properties.BooleanProperty`
+ and defaults to `False`.
+ """
+
+ disabled_color = ColorProperty(None)
+ """
+ Is active segment.
+
+ :attr:`active` is an :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
+ """
+
+ _no_ripple_effect = BooleanProperty(True)
+ _current_icon = ""
+ _current_md_bg_color = None
+
+ def on_disabled(self, instance, value: bool) -> None:
+ def on_disabled(*args):
+ if value:
+ if not self._current_md_bg_color:
+ self._current_md_bg_color = self.md_bg_color
+ self.md_bg_color = (
+ self.theme_cls.disabled_hint_text_color
+ if not self.disabled_color
+ else self.disabled_color
+ )
+ else:
+ if self._current_md_bg_color:
+ self.md_bg_color = self._current_md_bg_color
+ self._current_md_bg_color = None
+
+ Clock.schedule_once(on_disabled)
+
+ def on_icon(self, instance, icon_name: str):
+ if icon_name != "check":
+ self._current_icon = icon_name
+
+
+# TODO:
+# Add the feature to use both text and icons in segments -
+# https://m3.material.io/components/segmented-buttons/guidelines#26abac1c-c6bd-44c1-a969-8c910c880b98
+# Icons: optional check icon to indicate selected state -
+# https://m3.material.io/components/segmented-buttons/overview#7b80f313-7d3a-4865-b26c-1f7ec98ba694
+# Hovered: add a color for the hovered segment -
+# https://m3.material.io/components/segmented-buttons/specs#d730b3ba-c59e-4ef8-b652-20979fe20b67
+# Density: Each step down in density removes 4dp from the height -
+# https://m3.material.io/components/segmented-buttons/specs#2d5cab36-1deb-40bd-9e37-bc2bb1657009
+
+
+class MDSegmentedButton(MDBoxLayout):
+ """
+ Segment button panel.
+
+ For more information, see in the
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation.
+
+ :Events:
+ `on_marked`
+ The method is called when a segment is marked.
+ `on_unmarked`
+ The method is called when a segment is unmarked.
+ """
+
+ radius = VariableListProperty([20], length=4)
+ """
+ Panel radius.
+
+ :attr:`radius` is an :class:`~kivy.properties.VariableListProperty`
+ and defaults to `[20, 20, 20, 20]`.
+ """
+
+ multiselect = BooleanProperty(False)
+ """
+ Do I allow multiple segment selection.
+
+ :attr:`multiselect` is an :class:`~kivy.properties.BooleanProperty`
+ and defaults to `False`.
+ """
+
+ hiding_icon_transition = StringProperty("linear")
+ """
+ Name of the transition hiding the current icon.
+
+ :attr:`hiding_icon_transition` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `'linear'`.
+ """
+
+ hiding_icon_duration = NumericProperty(0.05)
+ """
+ Duration of hiding the current icon.
+
+ :attr:`hiding_icon_duration` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `0.05`.
+ """
+
+ opening_icon_transition = StringProperty("linear")
+ """
+ The name of the transition that opens a new icon of the "marked" type.
+
+ :attr:`opening_icon_transition` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `'linear'`.
+ """
+
+ opening_icon_duration = NumericProperty(0.05)
+ """
+ The duration of opening a new icon of the "marked" type.
+
+ :attr:`opening_icon_duration` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `0.05`.
+ """
+
+ selected_items = ListProperty()
+ """
+ The list of :class:`~MDSegmentedButtonItem` objects that are currently
+ marked.
+
+ :attr:`selected_items` is a :class:`~kivy.properties.ListProperty`
+ and defaults to `[]`.
+ """
+
+ selected_color = ColorProperty(None)
+ """
+ Color of the marked segment.
+
+ :attr:`selected_color` is a :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.register_event_type("on_marked")
+ self.register_event_type("on_unmarked")
+ Clock.schedule_once(self.mark_segment)
+ Clock.schedule_once(self.adjust_segment_radius)
+ Clock.schedule_once(self.adjust_segment_panel_width, 2)
+
+ def mark_segment(self, *args) -> None:
+ """Programmatically marks a segment."""
+
+ for widget in self.children:
+ if widget.active:
+ widget.active = False
+ widget.dispatch("on_release")
+
+ if not self.multiselect:
+ break
+
+ def adjust_segment_radius(self, *args) -> None:
+ """Rounds off the first and last elements."""
+
+ if self.children[0].radius == [0, 0, 0, 0]:
+ self.children[0].radius = (0, self.height / 2, self.height / 2, 0)
+ if self.children[-1].radius == [0, 0, 0, 0]:
+ self.children[-1].radius = (self.height / 2, 0, 0, self.height / 2)
+
+ def adjust_segment_panel_width(self, *args) -> None:
+ """
+ Sets the width of all segments and the width of the panel
+ by the widest segment.
+ """
+
+ if not self.size_hint_x:
+ width_list = [
+ widget.ids.label_text.texture_size[0]
+ + (dp(72) if widget.icon else dp(48))
+ for widget in self.children
+ ]
+ max_width = max(width_list)
+ self.width = max_width * len(width_list)
+ else:
+ max_width = self.width / len(self.children)
+
+ for widget in self.children:
+ widget.width = max_width
+
+ self.opacity = 1
+
+ for widget in self.children:
+ if widget.active:
+ widget.dispatch("on_release")
+
+ def shift_segment_text(self, segment_item: MDSegmentedButtonItem) -> None:
+ """
+ Shifts the segment text to the right, thus freeing up space
+ for the icon (when the segment is marked).
+ """
+
+ Animation(
+ x=(
+ segment_item.ids.label_text.x
+ + (
+ dp(16)
+ if not segment_item.icon and not segment_item.active
+ else 0
+ )
+ )
+ if not segment_item.active
+ else (
+ segment_item.ids.label_text.x
+ - (
+ dp(16)
+ if not segment_item.icon and segment_item.active
+ else 0
+ )
+ ),
+ d=0.2,
+ ).start(segment_item.ids.label_text)
+
+ def show_icon_marked_segment(
+ self, segment_item: MDSegmentedButtonItem
+ ) -> None:
+ """
+ Sets the icon for the marked segment and changes the icon scale
+ to the normal scale.
+ """
+
+ segment_item.ids.scale_icon.icon = "check"
+ if segment_item.ids.scale_icon.icon == "check" and segment_item.active:
+ segment_item.ids.scale_icon.icon = segment_item._current_icon
+
+ Animation(
+ scale_value_x=1,
+ scale_value_y=1,
+ d=self.opening_icon_duration,
+ t=self.opening_icon_transition,
+ ).start(segment_item.ids.scale_icon)
+
+ self.shift_segment_text(segment_item)
+ self.set_selected_segment_list(segment_item)
+ self.set_bg_marked_segment(segment_item)
+
+ def hide_icon_marked_segment(
+ self, segment_item: MDSegmentedButtonItem
+ ) -> None:
+ """Changes the scale of the icon of the marked segment to zero."""
+
+ anim = Animation(
+ scale_value_x=0,
+ scale_value_y=0,
+ d=self.hiding_icon_duration,
+ t=self.hiding_icon_transition,
+ )
+ anim.bind(
+ on_complete=lambda x, y: self.show_icon_marked_segment(segment_item)
+ )
+ anim.start(segment_item.ids.scale_icon)
+
+ def restore_bg_segment(self, segment_item) -> None:
+ Animation(md_bg_color=self.md_bg_color, d=0.2).start(segment_item)
+
+ def set_bg_marked_segment(self, segment_item) -> None:
+ if segment_item.active:
+ Animation(
+ md_bg_color=self.selected_color
+ if self.selected_color
+ else self.theme_cls.primary_color,
+ d=0.2,
+ ).start(segment_item)
+
+ def set_selected_segment_list(self, segment_item) -> None:
+ segment_item.active = not segment_item.active
+
+ if segment_item.active:
+ self.selected_items.append(segment_item)
+ self.dispatch("on_marked", segment_item, segment_item.active)
+ else:
+ if segment_item in self.selected_items:
+ self.selected_items.remove(segment_item)
+ self.dispatch("on_unmarked", segment_item, segment_item.active)
+
+ def mark_item(self, segment_item: MDSegmentedButtonItem) -> None:
+ if segment_item.active and not self.multiselect:
+ return
+ if not self.multiselect and self.selected_items:
+ self.uncheck_item()
+ else:
+ if segment_item.active:
+ self.restore_bg_segment(segment_item)
+
+ self.hide_icon_marked_segment(segment_item)
+
+ def uncheck_item(self) -> None:
+ for item in self.children:
+ if item.active:
+ self.hide_icon_marked_segment(item)
+ self.restore_bg_segment(item)
+ break
+
+ def add_widget(self, widget, *args, **kwargs):
+ if isinstance(widget, MDSegmentedButtonItem):
+ widget.bind(on_release=self.mark_item)
+ return super().add_widget(widget)
+
+ def on_size(self, instance_segment_button, size: list) -> None:
+ """Called when the root screen is resized."""
+
+ if self.size_hint_x:
+ max_width = size[0] / len(self.children)
+ for widget in self.children:
+ widget.width = max_width
+
+ def on_marked(self, *args):
+ """The method is called when a segment is marked."""
+
+ def on_unmarked(self, *args):
+ """The method is called when a segment is unmarked."""
+
+
+class SegmentButtonIcon(MDIcon, ScaleBehavior):
+ """Implements an icon with scaling behavior."""

diff --git a/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.kv b/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.kv
index 3ac36560..8ceb004d 100644
--- a/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.kv
+++ b/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.kv
@@ -1,3 +1,6 @@
+#:import SEGMENT_CONTROL_SEGMENT_SWITCH_ELEVATION kivymd.material_resources.SEGMENT_CONTROL_SEGMENT_SWITCH_ELEVATION
+
+
<MDSegmentedControlItem>
adaptive_height: True
halign: "center"
@@ -15,8 +18,9 @@
pos_hint: {"center_y": .5}
x: root._segment_switch_x
md_bg_color: root.segment_color
- elevation: 2
+ elevation: SEGMENT_CONTROL_SEGMENT_SWITCH_ELEVATION
_radius: root.radius[0] - 4
+ shadow_radius: self._radius
width:
segment_panel.width / segment_panel.children_number \
- segment_panel.spacing

diff --git a/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.py b/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.py
index 5e9c7f42..90db85cb 100644
--- a/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.py
+++ b/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.py
@@ -121,7 +121,6 @@ from kivy.properties import (
)
from kivymd import uix_path
-from kivymd.theming import ThemableBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import MDRaisedButton
from kivymd.uix.card import MDSeparator
@@ -145,12 +144,12 @@ class MDSegmentedControlItem(MDLabel):
# TODO: Add an attribute for the color of the active segment label.
-class MDSegmentedControl(MDRelativeLayout, ThemableBehavior):
+class MDSegmentedControl(MDRelativeLayout):
"""
Implements a segmented control panel.
- Relative layout class. For more information, see in the
- :class:`~kivy.uix.relativelayout.RelativeLayout` class documentation.
+ For more information, see in the
+ :class:`~kivymd.uix.relativelayout.MDRelativeLayout` class documentation.
:Events:
`on_active`
@@ -159,7 +158,7 @@ class MDSegmentedControl(MDRelativeLayout, ThemableBehavior):
md_bg_color = ColorProperty([0, 0, 0, 0])
"""
- Background color of the segment panel.
+ Background color of the segment panel in (r, g, b, a) or string format.
.. code-block:: kv
@@ -175,7 +174,7 @@ class MDSegmentedControl(MDRelativeLayout, ThemableBehavior):
segment_color = ColorProperty([0, 0, 0, 0])
"""
- Color of the active segment.
+ Color of the active segment in (r, g, b, a) or string format.
.. code-block:: kv
@@ -220,7 +219,8 @@ class MDSegmentedControl(MDRelativeLayout, ThemableBehavior):
separator_color = ColorProperty(None)
"""
- The color of the separator between the segments.
+ The color of the separator between the segments in (r, g, b, a) or string
+ format.
.. code-block:: kv

diff --git a/sbapp/kivymd/uix/selection/selection.py b/sbapp/kivymd/uix/selection/selection.py
index a82804fb..1ee86765 100644
--- a/sbapp/kivymd/uix/selection/selection.py
+++ b/sbapp/kivymd/uix/selection/selection.py
@@ -276,7 +276,6 @@ from kivy.properties import (
)
from kivymd import uix_path
-from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import TouchBehavior
from kivymd.uix.button import MDIconButton
from kivymd.uix.list import MDList
@@ -295,7 +294,7 @@ class SelectionIconCheck(MDIconButton):
icon_check_color = ColorProperty([0, 0, 0, 1])
-class SelectionItem(ThemableBehavior, MDRelativeLayout, TouchBehavior):
+class SelectionItem(MDRelativeLayout, TouchBehavior):
selected = BooleanProperty(False)
"""
Whether or not an item is checked.
@@ -514,6 +513,11 @@ class SelectionItem(ThemableBehavior, MDRelativeLayout, TouchBehavior):
class MDSelectionList(MDList):
"""
+ Selection list class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.list.MDList` classes documentation.
+
:Events:
`on_selected`
Called when a list item is selected.
@@ -548,7 +552,8 @@ class MDSelectionList(MDList):
icon_bg_color = ColorProperty([1, 1, 1, 1])
"""
- Background color of the icon that will mark the selected list item.
+ Background color in (r, g, b, a) or string format of the icon that will
+ mark the selected list item.
:attr:`icon_bg_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[1, 1, 1, 1]`.
@@ -556,7 +561,8 @@ class MDSelectionList(MDList):
icon_check_color = ColorProperty([0, 0, 0, 1])
"""
- Color of the icon that will mark the selected list item.
+ Color in (r, g, b, a) or string format of the icon that will mark the
+ selected list item.
:attr:`icon_check_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[1, 1, 1, 1]`.
@@ -564,7 +570,7 @@ class MDSelectionList(MDList):
overlay_color = ColorProperty([0, 0, 0, 0.2])
"""
- The overlay color of the selected list item..
+ The overlay color in (r, g, b, a) or string format of the selected list item.
:attr:`overlay_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[0, 0, 0, 0.2]]`.
@@ -580,7 +586,8 @@ class MDSelectionList(MDList):
progress_round_color = ColorProperty(None)
"""
- Color of the spinner for switching of `selected_mode` mode.
+ Color in (r, g, b, a) or string format of the spinner for switching of
+ `selected_mode` mode.
:attr:`progress_round_color` is an :class:`~kivy.properties.NumericProperty`
and defaults to `None`.

diff --git a/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.py b/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.py
index 031fd3a3..6bcefe67 100755
--- a/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.py
+++ b/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.py
@@ -4,12 +4,11 @@ Components/SelectionControls
.. seealso::
- `Material Design spec, Selection controls <https://material.io/components/selection-controls>`_
+ `Material Design spec, Checkbox <https://m3.material.io/components/checkbox/overview>`_
-.. rubric:: Selection controls allow the user to select options.
+ `Material Design spec, Switch <https://m3.material.io/components/switch/overview>`_
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selection-controll.png
- :align: center
+.. rubric:: Selection controls allow the user to select options.
`KivyMD` provides the following selection controls classes for use:
@@ -20,6 +19,12 @@ Components/SelectionControls
MDCheckbox
----------
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox.png
+ :align: center
+
+Usage
+-----
+
.. code-block:: python
from kivy.lang import Builder
@@ -37,18 +42,20 @@ MDCheckbox
'''
- class Test(MDApp):
+ class Example(MDApp):
def build(self):
+ self.theme_cls.primary_palette = "Green"
+ self.theme_cls.theme_style = "Dark"
return Builder.load_string(KV)
- Test().run()
+ Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox.gif
:align: center
.. Note:: Be sure to specify the size of the checkbox. By default, it is
- ``(dp(48), dp(48))``, but the ripple effect takes up all the available
+ `(dp(48), dp(48))`, but the ripple effect takes up all the available
space.
Control state
@@ -94,60 +101,137 @@ MDCheckbox with group
'''
- class Test(MDApp):
+ class Example(MDApp):
def build(self):
+ self.theme_cls.primary_palette = "Green"
+ self.theme_cls.theme_style = "Dark"
return Builder.load_string(KV)
- Test().run()
+ Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox-group.gif
:align: center
-.. MDSwitch:
-MDSwitch
---------
+Parent and child checkboxes
+---------------------------
+
+Checkboxes can have a parent-child relationship with other checkboxes. When
+the parent checkbox is checked, all child checkboxes are checked. If a parent
+checkbox is unchecked, all child checkboxes are unchecked. If some, but not all,
+child checkboxes are checked, the parent checkbox becomes an indeterminate
+checkbox.
+
+Usage
+-----
+
+.. code-block:: kv
+
+ MDCheckbox:
+ group: "root" # this is a required name for the parent checkbox group
+
+ MDCheckbox:
+ group: "child" # this is a required name for a group of child checkboxes
+
+ MDCheckbox:
+ group: "child" # this is a required name for a group of child checkboxes
+
+Example
+-------
.. code-block:: python
from kivy.lang import Builder
+ from kivy.properties import StringProperty
from kivymd.app import MDApp
+ from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
- MDFloatLayout:
+ <CheckItem>
+ adaptive_height: True
- MDSwitch:
- pos_hint: {'center_x': .5, 'center_y': .5}
+ MDCheckbox:
+ size_hint: None, None
+ size: "48dp", "48dp"
+ group: root.group
+
+ MDLabel:
+ text: root.text
+ adaptive_height: True
+ theme_text_color: "Custom"
+ text_color: "#B2B6AE"
+ pos_hint: {"center_y": .5}
+
+
+ MDBoxLayout:
+ orientation: "vertical"
+ md_bg_color: "#141612"
+
+ MDTopAppBar:
+ md_bg_color: "#21271F"
+ specific_text_color: "#B2B6AE"
+ elevation: 0
+ title: "Meal options"
+ left_action_items: [["arrow-left", lambda x: x]]
+ anchor_title: "left"
+
+ MDBoxLayout:
+ orientation: "vertical"
+ adaptive_height: True
+ padding: "12dp", "36dp", 0, 0
+
+ CheckItem:
+ text: "Recieve emails"
+ group: "root"
+
+ MDBoxLayout:
+ orientation: "vertical"
+ adaptive_height: True
+ padding: "24dp", 0, 0, 0
+
+ CheckItem:
+ text: "Daily"
+ group: "child"
+
+ CheckItem:
+ text: "Weekly"
+ group: "child"
+
+ CheckItem:
+ text: "Monthly"
+ group: "child"
+
+ MDWidget:
'''
- class Test(MDApp):
+ class CheckItem(MDBoxLayout):
+ text = StringProperty()
+ group = StringProperty()
+
+
+ class Example(MDApp):
def build(self):
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Teal"
return Builder.load_string(KV)
- Test().run()
+ Example().run()
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-switch.gif
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox-parent-child.gif
:align: center
-.. Note:: For :class:`~MDSwitch` size is not required. By default it is
- ``(dp(36), dp(48))``, but you can increase the width if you want.
-
-.. code-block:: kv
-
- MDSwitch:
- width: dp(64)
+.. MDSwitch:
+MDSwitch
+--------
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-switch_width.png
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/switch.png
:align: center
-.. Note:: Control state of :class:`~MDSwitch` same way as in
- :class:`~MDCheckbox`.
-
-MDSwitch in M3 style
---------------------
+Usage
+-----
.. code-block:: python
@@ -156,24 +240,27 @@ MDSwitch in M3 style
from kivymd.app import MDApp
KV = '''
- MDScreen:
+ MDFloatLayout:
MDSwitch:
pos_hint: {'center_x': .5, 'center_y': .5}
- active: True
'''
- class Test(MDApp):
+ class Example(MDApp):
def build(self):
- self.theme_cls.material_style = "M3"
+ self.theme_cls.primary_palette = "Green"
+ self.theme_cls.theme_style = "Dark"
return Builder.load_string(KV)
- Test().run()
+ Example().run()
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox-m3.gif
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-switch.gif
:align: center
+
+.. Note:: Control state of :class:`~MDSwitch` same way as in
+ :class:`~MDCheckbox`.
"""
__all__ = ("MDCheckbox", "MDSwitch")
@@ -195,9 +282,14 @@ from kivy.uix.floatlayout import FloatLayout
from kivymd import uix_path
from kivymd.theming import ThemableBehavior
-from kivymd.uix.behaviors import CircularRippleBehavior, CommonElevationBehavior
+from kivymd.uix.behaviors import (
+ CircularRippleBehavior,
+ CommonElevationBehavior,
+ ScaleBehavior,
+)
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.label import MDIcon
+from kivymd.utils import asynckivy
with open(
os.path.join(uix_path, "selectioncontrol", "selectioncontrol.kv"),
@@ -206,7 +298,22 @@ with open(
Builder.load_string(kv_file.read())
-class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon):
+class MDCheckbox(
+ CircularRippleBehavior, ScaleBehavior, ToggleButtonBehavior, MDIcon
+):
+ """
+ Checkbox class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and
+ :class:`~kivy.uix.behaviors.ToggleButtonBehavior` and
+ :class:`~kivymd.uix.label.MDIcon`
+ classes documentation.
+ """
+
+ __allow_child_checkboxes_active = True
+ __allow_root_checkbox_active = True
+
active = BooleanProperty(False)
"""
Indicates if the checkbox is active or inactive.
@@ -235,7 +342,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon):
radio_icon_normal = StringProperty("checkbox-blank-circle-outline")
"""
- Background icon (when using the ``group`` option) of the checkbox used for
+ Background icon (when using the `group` option) of the checkbox used for
the default graphical representation when the checkbox is not pressed.
:attr:`radio_icon_normal` is a :class:`~kivy.properties.StringProperty`
@@ -244,7 +351,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon):
radio_icon_down = StringProperty("checkbox-marked-circle")
"""
- Background icon (when using the ``group`` option) of the checkbox used for
+ Background icon (when using the `group` option) of the checkbox used for
the default graphical representation when the checkbox is pressed.
:attr:`radio_icon_down` is a :class:`~kivy.properties.StringProperty`
@@ -253,7 +360,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon):
color_active = ColorProperty(None)
"""
- Color when the checkbox is in the active state.
+ Color in (r, g, b, a) or string format when the checkbox is in the active state.
.. versionadded:: 1.0.0
@@ -271,7 +378,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon):
color_inactive = ColorProperty(None)
"""
- Color when the checkbox is in the inactive state.
+ Color in (r, g, b, a) or string format when the checkbox is in the inactive state.
.. versionadded:: 1.0.0
@@ -289,7 +396,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon):
disabled_color = ColorProperty(None)
"""
- Color when the checkbox is in the disabled state.
+ Color in (r, g, b, a) or string format when the checkbox is in the disabled state.
.. code-block:: kv
@@ -309,7 +416,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon):
selected_color = ColorProperty(None, deprecated=True)
"""
- Color when the checkbox is in the active state.
+ Color in (r, g, b, a) or string format when the checkbox is in the active state.
.. deprecated:: 1.0.0
Use :attr:`color_active` instead.
@@ -320,7 +427,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon):
unselected_color = ColorProperty(None, deprecated=True)
"""
- Color when the checkbox is in the inactive state.
+ Color in (r, g, b, a) or string format when the checkbox is in the inactive state.
.. deprecated:: 1.0.0
Use :attr:`color_inactive` instead.
@@ -332,9 +439,11 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon):
_current_color = ColorProperty([0.0, 0.0, 0.0, 0.0])
def __init__(self, **kwargs):
- self.check_anim_out = Animation(font_size=0, duration=0.1, t="out_quad")
+ self.check_anim_out = Animation(
+ scale_value_x=0, scale_value_y=0, duration=0.1, t="out_quad"
+ )
self.check_anim_in = Animation(
- font_size=sp(24), duration=0.1, t="out_quad"
+ scale_value_x=1, scale_value_y=1, duration=0.1, t="out_quad"
)
super().__init__(**kwargs)
self.color_active = self.theme_cls.primary_color
@@ -364,6 +473,13 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon):
self.update_color()
def update_primary_color(self, instance, value) -> None:
+ """
+ Called when the values of
+ :attr:`kivymd.theming.ThemableBehavior.theme_cls.theme_style` and
+ :attr:`kivymd.theming.ThemableBehavior.theme_cls.primary_color`
+ change.
+ """
+
if value in ("Dark", "Light"):
if not self.disabled:
self.color = self.theme_cls.primary_color
@@ -373,18 +489,41 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon):
self.color_active = value
def update_icon(self, *args) -> None:
+ """
+ Called when the values of
+ :attr:`checkbox_icon_normal` and
+ :attr:`checkbox_icon_down` and
+ :attr:`radio_icon_normal` and
+ :attr:`group`
+ change.
+ """
+
if self.state == "down":
self.icon = (
- self.radio_icon_down if self.group else self.checkbox_icon_down
+ self.radio_icon_down
+ if self.group and self.group not in ["root", "child"]
+ else self.checkbox_icon_down
+ if self.group != "root"
+ else "minus-box"
)
else:
self.icon = (
self.radio_icon_normal
- if self.group
+ if self.group and self.group not in ["root", "child"]
else self.checkbox_icon_normal
)
def update_color(self, *args) -> None:
+ """
+ Called when the values of
+ :attr:`color_active` and
+ :attr:`color_inactive` and
+ :attr:`disabled_color` and
+ :attr:`disabled` and
+ :attr:`state`
+ change.
+ """
+
if self.disabled:
self._current_color = self.disabled_color
elif self.state == "down":
@@ -393,6 +532,8 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon):
self._current_color = self.color_inactive
def on_state(self, *args) -> None:
+ """Called when the values of :attr:`state` change."""
+
if self.state == "down":
self.check_anim_in.cancel(self)
self.check_anim_out.start(self)
@@ -408,8 +549,45 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon):
self.active = False
def on_active(self, *args) -> None:
+ """Called when the values of :attr:`active` change."""
+
self.state = "down" if self.active else "normal"
+ if (
+ self.group
+ and self.group == "root"
+ and MDCheckbox.__allow_root_checkbox_active
+ ):
+ self.set_child_active(self.active)
+ elif self.group and self.group == "child":
+ if MDCheckbox.__allow_child_checkboxes_active:
+ self.set_root_active()
+
+ def set_root_active(self) -> None:
+ root_checkbox = self.get_widgets("root")
+ if root_checkbox:
+ MDCheckbox.__allow_root_checkbox_active = False
+ root_checkbox[0].active = True in [
+ child.active for child in self.get_widgets("child")
+ ]
+ MDCheckbox.__allow_root_checkbox_active = True
+
+ def set_child_active(self, active: bool):
+ for child in self.get_widgets("child"):
+ child.active = active
+ MDCheckbox.__allow_child_checkboxes_active = True
+
+ def on_touch_down(self, touch):
+ if self.collide_point(touch.x, touch.y):
+ if self.group and self.group == "root":
+ MDCheckbox.__allow_child_checkboxes_active = False
+ return super().on_touch_down(touch)
+
+ def _release_group(self, current):
+ if self.group and self.group in ["root", "child"]:
+ return
+ super()._release_group(current)
+
class ThumbIcon(MDIcon):
"""
@@ -419,14 +597,8 @@ class ThumbIcon(MDIcon):
"""
-class Thumb(
- CommonElevationBehavior,
- CircularRippleBehavior,
- MDFloatLayout,
-):
- """
- Implements a thumb for the :class:`~MDSwitch` widget.
- """
+class Thumb(CommonElevationBehavior, CircularRippleBehavior, MDFloatLayout):
+ """Implements a thumb for the :class:`~MDSwitch` widget."""
def _set_ellipse(self, instance, value):
self.ellipse.size = (self._ripple_rad, self._ripple_rad)
@@ -443,6 +615,14 @@ class Thumb(
class MDSwitch(ThemableBehavior, FloatLayout):
+ """
+ Switch class.
+
+ For more information, see in the
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivy.uix.floatlayout.FloatLayout` classes documentation.
+ """
+
active = BooleanProperty(False)
"""
Indicates if the switch is active or inactive.
@@ -490,7 +670,8 @@ class MDSwitch(ThemableBehavior, FloatLayout):
icon_active_color = ColorProperty(None)
"""
- Thumb icon color when the switch is in the active state (only M3 style).
+ Thumb icon color in (r, g, b, a) or string format when the switch is in the
+ active state (only M3 style).
.. versionadded:: 1.0.0
@@ -510,7 +691,8 @@ class MDSwitch(ThemableBehavior, FloatLayout):
icon_inactive_color = ColorProperty(None)
"""
- Thumb icon color when the switch is in an inactive state (only M3 style).
+ Thumb icon color in (r, g, b, a) or string format when the switch is in an
+ inactive state (only M3 style).
.. versionadded:: 1.0.0
@@ -529,7 +711,7 @@ class MDSwitch(ThemableBehavior, FloatLayout):
thumb_color_active = ColorProperty(None)
"""
- The color of the thumb when the switch is active.
+ The color in (r, g, b, a) or string format of the thumb when the switch is active.
.. versionadded:: 1.0.0
@@ -548,7 +730,7 @@ class MDSwitch(ThemableBehavior, FloatLayout):
thumb_color_inactive = ColorProperty(None)
"""
- The color of the thumb when the switch is inactive.
+ The color in (r, g, b, a) or string format of the thumb when the switch is inactive.
.. versionadded:: 1.0.0
@@ -566,7 +748,8 @@ class MDSwitch(ThemableBehavior, FloatLayout):
thumb_color_disabled = ColorProperty(None)
"""
- The color of the thumb when the switch is in the disabled state.
+ The color in (r, g, b, a) or string format of the thumb when the switch is
+ in the disabled state.
.. code-block:: kv
@@ -584,7 +767,7 @@ class MDSwitch(ThemableBehavior, FloatLayout):
track_color_active = ColorProperty(None)
"""
- The color of the track when the switch is active.
+ The color in (r, g, b, a) or string format of the track when the switch is active.
.. code-block:: kv
@@ -601,7 +784,7 @@ class MDSwitch(ThemableBehavior, FloatLayout):
track_color_inactive = ColorProperty(None)
"""
- The color of the track when the switch is inactive.
+ The color in (r, g, b, a) or string format of the track when the switch is inactive.
.. versionadded:: 1.0.0
@@ -619,7 +802,8 @@ class MDSwitch(ThemableBehavior, FloatLayout):
track_color_disabled = ColorProperty(None)
"""
- The color of the track when the switch is in the disabled state.
+ The color in (r, g, b, a) or string format of the track when the switch is
+ in the disabled state.
.. code-block:: kv
@@ -646,6 +830,11 @@ class MDSwitch(ThemableBehavior, FloatLayout):
Clock.schedule_once(lambda x: self.on_active(self, self.active))
def set_icon(self, instance_switch, icon_value: str) -> None:
+ """
+ Called when the values of
+ :attr:`icon_active` and :attr:`icon_inactive` change.
+ """
+
def set_icon(*args):
icon = icon_value if icon_value else "blank"
self.ids.thumb.ids.icon.icon = icon
@@ -653,6 +842,8 @@ class MDSwitch(ThemableBehavior, FloatLayout):
Clock.schedule_once(set_icon, 0.2)
def on_active(self, instance_switch, active_value: bool) -> None:
+ """Called when the values of :attr:`active` change."""
+
if self.theme_cls.material_style == "M3" and self.widget_style != "ios":
size = (
(

diff --git a/sbapp/kivymd/uix/slider/slider.py b/sbapp/kivymd/uix/slider/slider.py
index 152a602a..f0824ae3 100644
--- a/sbapp/kivymd/uix/slider/slider.py
+++ b/sbapp/kivymd/uix/slider/slider.py
@@ -52,7 +52,7 @@ class MDSlider(ThemableBehavior, Slider):
color = ColorProperty(None)
"""
- Color slider.
+ Color slider in (r, g, b, a) or string format.
.. code-block:: kv
@@ -84,7 +84,7 @@ class MDSlider(ThemableBehavior, Slider):
hint_bg_color = ColorProperty(None)
"""
- Hint rectangle color in (r.g.b.a) format.
+ Hint rectangle color in (r, g, b, a) or string format.
.. code-block:: kv
@@ -101,7 +101,7 @@ class MDSlider(ThemableBehavior, Slider):
hint_text_color = ColorProperty(None)
"""
- Hint text color in (r.g.b.a) format.
+ Hint text color in in (r, g, b, a) or string format.
.. code-block:: kv
@@ -138,7 +138,7 @@ class MDSlider(ThemableBehavior, Slider):
thumb_color_active = ColorProperty(None)
"""
- The color of the thumb when the slider is active.
+ The color in (r, g, b, a) or string format of the thumb when the slider is active.
.. versionadded:: 1.0.0
@@ -156,7 +156,7 @@ class MDSlider(ThemableBehavior, Slider):
thumb_color_inactive = ColorProperty(None)
"""
- The color of the thumb when the slider is inactive.
+ The color in (r, g, b, a) or string format of the thumb when the slider is inactive.
.. versionadded:: 1.0.0
@@ -174,7 +174,8 @@ class MDSlider(ThemableBehavior, Slider):
thumb_color_disabled = ColorProperty(None)
"""
- The color of the thumb when the slider is in the disabled state.
+ The color in (r, g, b, a) or string format of the thumb when the slider is
+ in the disabled state.
.. versionadded:: 1.0.0
@@ -194,7 +195,7 @@ class MDSlider(ThemableBehavior, Slider):
track_color_active = ColorProperty(None)
"""
- The color of the track when the slider is active.
+ The color in (r, g, b, a) or string format of the track when the slider is active.
.. versionadded:: 1.0.0
@@ -212,7 +213,7 @@ class MDSlider(ThemableBehavior, Slider):
track_color_inactive = ColorProperty(None)
"""
- The color of the track when the slider is inactive.
+ The color in (r, g, b, a) or string format of the track when the slider is inactive.
.. versionadded:: 1.0.0
@@ -230,7 +231,8 @@ class MDSlider(ThemableBehavior, Slider):
track_color_disabled = ColorProperty(None)
"""
- The color of the track when the slider is in the disabled state.
+ The color in (r, g, b, a) or string format of the track when the slider is
+ in the disabled state.
.. versionadded:: 1.0.0

diff --git a/sbapp/kivymd/uix/sliverappbar/sliverappbar.py b/sbapp/kivymd/uix/sliverappbar/sliverappbar.py
index 6316315e..f8e8ab54 100644
--- a/sbapp/kivymd/uix/sliverappbar/sliverappbar.py
+++ b/sbapp/kivymd/uix/sliverappbar/sliverappbar.py
@@ -97,7 +97,7 @@ Example
class CardItem(MDCard):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.elevation = 3
+ self.elevation = 1
class Example(MDApp):
@@ -130,7 +130,6 @@ from kivy.properties import (
)
from kivymd import uix_path
-from kivymd.theming import ThemableBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.toolbar import MDTopAppBar
@@ -144,8 +143,13 @@ class MDSliverAppbarException(Exception):
pass
-class MDSliverAppbarContent(ThemableBehavior, MDBoxLayout):
- """Implements a box for a scrollable list of custom items."""
+class MDSliverAppbarContent(MDBoxLayout):
+ """
+ Implements a box for a scrollable list of custom items.
+
+ For more information, see in the
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation.
+ """
md_bg_color = ColorProperty([0, 0, 0, 0])
"""
@@ -165,13 +169,20 @@ class MDSliverAppbarContent(ThemableBehavior, MDBoxLayout):
class MDSliverAppbarHeader(MDBoxLayout):
- pass
+ """
+ Sliver app bar header class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation.
+ """
-class MDSliverAppbar(MDBoxLayout, ThemableBehavior):
+class MDSliverAppbar(MDBoxLayout):
"""
- MDSliverAppbar class.
- See module documentation for more information.
+ Sliver app bar class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation.
:Events:
:attr:`on_scroll_content`
@@ -254,7 +265,7 @@ class MDSliverAppbar(MDBoxLayout, ThemableBehavior):
class CardItem(MDCard):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.elevation = 3
+ self.elevation = 1
class SliverToolbar(MDTopAppBar):
@@ -292,7 +303,7 @@ class MDSliverAppbar(MDBoxLayout, ThemableBehavior):
background_color = ColorProperty(None)
"""
- Background color of toolbar in (r, g, b, a) format.
+ Background color of toolbar in (r, g, b, a) or string format.
.. code-block:: kv

diff --git a/sbapp/kivymd/uix/snackbar/__init__.py b/sbapp/kivymd/uix/snackbar/__init__.py
index ed3c68d4..a6fd8ec8 100644
--- a/sbapp/kivymd/uix/snackbar/__init__.py
+++ b/sbapp/kivymd/uix/snackbar/__init__.py
@@ -1 +1,6 @@
-from .snackbar import BaseSnackbar, Snackbar # NOQA F401
+from .snackbar import ( # NOQA F401
+ MDSnackbar,
+ MDSnackbarActionButton,
+ MDSnackbarCloseButton,
+ Snackbar,
+)

diff --git a/sbapp/kivymd/uix/snackbar/snackbar.kv b/sbapp/kivymd/uix/snackbar/snackbar.kv
index 557ceda7..4334bbf3 100644
--- a/sbapp/kivymd/uix/snackbar/snackbar.kv
+++ b/sbapp/kivymd/uix/snackbar/snackbar.kv
@@ -1,34 +1,28 @@
-#:import window kivy.core.window
+#:import SNACK_BAR_ELEVATION kivymd.material_resources.SNACK_BAR_ELEVATION
+#:import SNACK_BAR_OFFSET kivymd.material_resources.SNACK_BAR_OFFSET
-<BaseSnackbar>
+<MDSnackbar>
+ padding: 0, 0, "8dp", 0
size_hint_y: None
- height: "58dp"
- spacing: "10dp"
- padding: "10dp", "10dp", "10dp", "10dp"
- md_bg_color: "323232" if not root.bg_color else root.bg_color
- radius: root.radius
- elevation: 4 if root.padding else 0
+ height: self.minimum_height
+ md_bg_color: "#323232"
+ elevation: SNACK_BAR_ELEVATION
+ shadow_offset: SNACK_BAR_OFFSET
- canvas:
- Color:
- rgba: self.md_bg_color
- RoundedRectangle:
- size: self.size
- pos: self.pos
- radius: self.radius
+ SnackbarLabelContainer:
+ id: label_container
+ padding: "16dp", "15dp", 0, "15dp"
+ orientation: "vertical"
+ adaptive_height: True
+ pos_hint: {"center_y": .5}
+ spacing: "4dp"
-<Snackbar>
+ SnackbarActionButtonContainer:
+ id: action_container
+ size_hint_x: None
- MDLabel:
- id: text_bar
- size_hint_y: None
- height: self.texture_size[1]
- text: root.text
- font_size: root.font_size
- theme_text_color: "Custom"
- text_color: "ffffff"
- shorten: True
- shorten_from: "right"
- markup: True
- pos_hint: {"center_y": .5}
+ SnackbarCloseButtonContainer:
+ id: close_container
+ size_hint_x: None
+ width: "38dp"

diff --git a/sbapp/kivymd/uix/snackbar/snackbar.py b/sbapp/kivymd/uix/snackbar/snackbar.py
index c4fbdc97..fdb7ebe4 100755
--- a/sbapp/kivymd/uix/snackbar/snackbar.py
+++ b/sbapp/kivymd/uix/snackbar/snackbar.py
@@ -4,7 +4,7 @@ Components/Snackbar
.. seealso::
- `Material Design spec, Snackbars <https://material.io/components/snackbars>`_
+ `Material Design spec, Snackbars <https://m3.material.io/components/snackbar/overview>`_
.. rubric:: Snackbars provide brief messages about app processes at the bottom
of the screen.
@@ -15,261 +15,281 @@ Components/Snackbar
Usage
-----
+.. code-block:: python
+
+ MDSnackbar(
+ MDLabel(
+ text="First string",
+ theme_text_color="Custom",
+ text_color="#393231",
+ ),
+ ).open()
+
+Example
+-------
+
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
-
- KV = '''
- #:import Snackbar kivymd.uix.snackbar.Snackbar
+ from kivymd.uix.label import MDLabel
+ from kivymd.uix.snackbar import MDSnackbar
+ KV = '''
MDScreen:
MDRaisedButton:
text: "Create simple snackbar"
- on_release: Snackbar(text="This is a snackbar!").open()
+ on_release: app.open_snackbar()
pos_hint: {"center_x": .5, "center_y": .5}
'''
- class Test(MDApp):
+ class Example(MDApp):
+ def open_snackbar(self):
+ MDSnackbar(
+ MDLabel(
+ text="First string",
+ ),
+ ).open()
+
def build(self):
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
return Builder.load_string(KV)
- Test().run()
+ Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-simple.gif
:align: center
-Usage with snackbar_x, snackbar_y
----------------------------------
+Control width and pos
+---------------------
.. code-block:: python
- Snackbar(
- text="This is a snackbar!",
- snackbar_x="10dp",
- snackbar_y="10dp",
- size_hint_x=(
- Window.width - (dp(10) * 2)
- ) / Window.width
+ MDSnackbar(
+ MDLabel(
+ text="First string",
+ ),
+ pos=(dp(24), dp(56)),
+ size_hint_x=0.5,
).open()
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-padding.gif
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-widith-and-pos.gif
:align: center
-Control width
--------------
+On mobile, use up to two lines of text to communicate the snackbar message:
.. code-block:: python
- Snackbar(
- text="This is a snackbar!",
- snackbar_x="10dp",
- snackbar_y="10dp",
- size_hint_x=.5
+ MDSnackbar(
+ MDLabel(
+ text="First string",
+ theme_text_color="Custom",
+ text_color="#393231",
+ ),
+ MDLabel(
+ text="Second string",
+ theme_text_color="Custom",
+ text_color="#393231",
+ ),
+ y=dp(24),
+ pos_hint={"center_x": 0.5},
+ size_hint_x=0.5,
+ md_bg_color="#E8D8D7",
).open()
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-percent-width.png
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-two-line.gif
:align: center
-Custom text color
------------------
-
-.. code-block:: python
-
- Snackbar(
- text="[color=#ddbb34]This is a snackbar![/color]",
- snackbar_y="10dp",
- snackbar_y="10dp",
- size_hint_x=.7
- ).open()
-
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-custom-color.png
- :align: center
+Usage action button
+-------------------
-Usage with button
------------------
+A snackbar can contain a single action. "Dismiss" or "cancel" actions are
+optional:
.. code-block:: python
- snackbar = Snackbar(
- text="This is a snackbar!",
- snackbar_x="10dp",
- snackbar_y="10dp",
- )
- snackbar.size_hint_x = (
- Window.width - (snackbar.snackbar_x * 2)
- ) / Window.width
- snackbar.buttons = [
- MDFlatButton(
- text="UPDATE",
- text_color=(1, 1, 1, 1),
- on_release=snackbar.dismiss,
+ MDSnackbar(
+ MDLabel(
+ text="First string",
+ theme_text_color="Custom",
+ text_color="#393231",
),
- MDFlatButton(
- text="CANCEL",
- text_color=(1, 1, 1, 1),
- on_release=snackbar.dismiss,
+ MDSnackbarActionButton(
+ text="Done",
+ theme_text_color="Custom",
+ text_color="#8E353C",
),
- ]
- snackbar.open()
-
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-button.png
- :align: center
-
-Using a button with custom color
---------------------------------
-
-.. code-block:: python
-
- Snackbar(
- ...
- bg_color=(0, 0, 1, 1),
+ y=dp(24),
+ pos_hint={"center_x": 0.5},
+ size_hint_x=0.5,
+ md_bg_color="#E8D8D7",
).open()
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-button-custom-color.png
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-action-button.gif
:align: center
-Custom usage
-------------
+Callback action button
+----------------------
.. code-block:: python
- from kivy.lang import Builder
- from kivy.animation import Animation
- from kivy.clock import Clock
- from kivy.metrics import dp
-
- from kivymd.app import MDApp
- from kivymd.uix.snackbar import Snackbar
-
-
- KV = '''
- MDScreen:
-
- MDFloatingActionButton:
- id: button
- x: root.width - self.width - dp(10)
- y: dp(10)
- on_release: app.snackbar_show()
- '''
+ def snackbar_action_button_callback(self, *args):
+ print("Snackbar callback action button")
+
+ def open_snackbar(self):
+ self.snackbar = MDSnackbar(
+ MDLabel(
+ text="First string",
+ theme_text_color="Custom",
+ text_color="#393231",
+ ),
+ MDSnackbarActionButton(
+ text="Done",
+ theme_text_color="Custom",
+ text_color="#8E353C",
+ _no_ripple_effect=True,
+ on_release=self.snackbar_action_button_callback,
+ ),
+ y=dp(24),
+ pos_hint={"center_x": 0.5},
+ size_hint_x=0.5,
+ md_bg_color="#E8D8D7",
+ )
+ self.snackbar.open()
+
+If an action is long, it can be displayed on a third line:
+.. code-block:: python
- class Test(MDApp):
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.screen = Builder.load_string(KV)
- self.snackbar = None
- self._interval = 0
+ MDSnackbar(
+ MDLabel(
+ text="If an action is long, it can be displayed",
+ theme_text_color="Custom",
+ text_color="#393231",
+ ),
+ MDLabel(
+ text="on a third line.",
+ theme_text_color="Custom",
+ text_color="#393231",
+ ),
+ MDLabel(
+ text=" ",
+ ),
+ MDSnackbarActionButton(
+ text="Action button",
+ theme_text_color="Custom",
+ text_color="#8E353C",
+ y=dp(8),
+ _no_ripple_effect=True,
+ ),
+ y=dp(24),
+ pos_hint={"center_x": 0.5},
+ size_hint_x=0.5,
+ md_bg_color="#E8D8D7",
+ ).open()
- def build(self):
- return self.screen
-
- def wait_interval(self, interval):
- self._interval += interval
- if self._interval > self.snackbar.duration + 0.5:
- anim = Animation(y=dp(10), d=.2)
- anim.start(self.screen.ids.button)
- Clock.unschedule(self.wait_interval)
- self._interval = 0
- self.snackbar = None
-
- def snackbar_show(self):
- if not self.snackbar:
- self.snackbar = Snackbar(text="This is a snackbar!")
- self.snackbar.open()
- anim = Animation(y=dp(72), d=.2)
- anim.bind(on_complete=lambda *args: Clock.schedule_interval(
- self.wait_interval, 0))
- anim.start(self.screen.ids.button)
-
-
- Test().run()
-
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-custom-usage.gif
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-action-button-on-thrid-line.gif
:align: center
-Custom Snackbar
----------------
+Icon (optional close affordance):
.. code-block:: python
- from kivy.lang import Builder
- from kivy.core.window import Window
- from kivy.properties import StringProperty, NumericProperty
-
- from kivymd.app import MDApp
- from kivymd.uix.button import MDFlatButton
- from kivymd.uix.snackbar import BaseSnackbar
-
- KV = '''
- <CustomSnackbar>
-
- MDIconButton:
- pos_hint: {'center_y': .5}
- icon: root.icon
- opposite_colors: True
-
- MDLabel:
- id: text_bar
- size_hint_y: None
- height: self.texture_size[1]
- text: root.text
- font_size: root.font_size
- theme_text_color: 'Custom'
- text_color: 'ffffff'
- shorten: True
- shorten_from: 'right'
- pos_hint: {'center_y': .5}
-
-
- MDScreen:
-
- MDRaisedButton:
- text: "SHOW"
- pos_hint: {"center_x": .5, "center_y": .45}
- on_press: app.show()
- '''
-
+ def snackbar_close(self, *args):
+ self.snackbar.dismiss()
+
+ def open_snackbar(self):
+ self.snackbar = MDSnackbar(
+ MDLabel(
+ text="Icon (optional close affordance)",
+ theme_text_color="Custom",
+ text_color="#393231",
+ ),
+ MDSnackbarActionButton(
+ text="Action button",
+ theme_text_color="Custom",
+ text_color="#8E353C",
+ _no_ripple_effect=True,
+ ),
+ MDSnackbarCloseButton(
+ icon="close",
+ theme_text_color="Custom",
+ text_color="#8E353C",
+ _no_ripple_effect=True,
+ on_release=self.snackbar_close,
+ ),
+ y=dp(24),
+ pos_hint={"center_x": 0.5},
+ size_hint_x=0.5,
+ md_bg_color="#E8D8D7",
+ )
+ self.snackbar.open()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-optional-close-affordance.gif
+ :align: center
- class CustomSnackbar(BaseSnackbar):
- text = StringProperty(None)
- icon = StringProperty(None)
- font_size = NumericProperty("15sp")
+API break
+=========
+1.1.1 version
+-------------
- class Test(MDApp):
- def build(self):
- return Builder.load_string(KV)
+.. code-block:: python
- def show(self):
- snackbar = CustomSnackbar(
- text="This is a snackbar!",
- icon="information",
- snackbar_x="10dp",
- snackbar_y="10dp",
- buttons=[MDFlatButton(text="ACTION", text_color=(1, 1, 1, 1))]
- )
- snackbar.size_hint_x = (
- Window.width - (snackbar.snackbar_x * 2)
- ) / Window.width
- snackbar.open()
+ snackbar = Snackbar(
+ text="First string",
+ snackbar_x="10dp",
+ snackbar_y="24dp",
+ )
+ snackbar.size_hint_x = (
+ Window.width - (snackbar.snackbar_x * 2)
+ ) / Window.width
+ snackbar.buttons = [
+ MDFlatButton(
+ text="Done",
+ theme_text_color="Custom",
+ text_color="#8E353C",
+ on_release=snackbar.dismiss,
+ ),
+ ]
+ snackbar.open()
+1.2.0 version
+-------------
- Test().run()
+.. code-block:: python
-.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-custom.png
- :align: center
+ MDSnackbar(
+ MDLabel(
+ text="First string",
+ ),
+ MDSnackbarActionButton(
+ text="Done",
+ theme_text_color="Custom",
+ text_color="#8E353C",
+ ),
+ y=dp(24),
+ pos_hint={"center_x": 0.5},
+ size_hint_x=0.5,
+ md_bg_color="#E8D8D7",
+ ).open()
"""
-__all__ = ("Snackbar", "BaseSnackbar")
+__all__ = (
+ "MDSnackbar",
+ "MDSnackbarActionButton",
+ "MDSnackbarCloseButton",
+)
import os
+from kivy import Logger
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.core.window import Window
@@ -284,8 +304,12 @@ from kivy.properties import (
)
from kivymd import uix_path
-from kivymd.uix.button import BaseButton
+from kivymd.uix.behaviors import MotionShackBehavior
+from kivymd.uix.boxlayout import MDBoxLayout
+from kivymd.uix.button import MDFlatButton, MDIconButton
from kivymd.uix.card import MDCard
+from kivymd.uix.label import MDLabel
+from kivymd.uix.relativelayout import MDRelativeLayout
with open(
os.path.join(uix_path, "snackbar", "snackbar.kv"), encoding="utf-8"
@@ -293,31 +317,63 @@ with open(
Builder.load_string(kv_file.read())
-class BaseSnackbar(MDCard):
+class SnackbarLabelContainer(MDBoxLayout):
+ """Container for placing snackbar text."""
+
+
+class SnackbarActionButtonContainer(MDRelativeLayout):
+ """Container for placing snackbar action button."""
+
+
+class SnackbarCloseButtonContainer(MDRelativeLayout):
+ """Container for placing snackbar close button."""
+
+
+class MDSnackbarCloseButton(MDIconButton):
+ """
+ Snackbar closed button class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.button.MDIconButton` class documentation.
"""
- :Events:
- :attr:`on_open`
- Called when a dialog is opened.
- :attr:`on_dismiss`
- When the front layer rises.
- Abstract base class for all Snackbars.
- This class handles sizing, positioning, shape and events for Snackbars
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if not self.y and not self.pos_hint:
+ self.pos_hint = {"center_y": 0.5}
+
+
+class MDSnackbarActionButton(MDFlatButton):
+ """
+ Snackbar action button class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.button.MDFlatButton` class documentation.
+ """
- All Snackbars will be made off of this `BaseSnackbar`.
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if not self.y and not self.pos_hint:
+ self.pos_hint = {"center_y": 0.5}
- `BaseSnackbar` will always try to fill the remainder of the screen with
- your Snackbar.
- To make your Snackbar dynamic and symetric with snackbar_x.
+class MDSnackbar(MotionShackBehavior, MDCard):
+ """
+ Snackbar class.
- Set size_hint_x like below:
+ .. versionchanged:: 1.2.0
+ Rename `BaseSnackbar` to `MDSnackbar` class.
- .. code-block:: python
+ For more information, see in the
+ :class:`~kivymd.uix.card.MDCard` and
+ :class:`~kivymd.uix.behaviors.StencilBehavior`
+ class documentation.
- size_hint_z = (
- Window.width - (snackbar_x * 2)
- ) / Window.width
+ :Events:
+ :attr:`on_open`
+ Called when a snackbar opened.
+ :attr:`on_dismiss`
+ Called when a snackbar closes.
"""
duration = NumericProperty(3)
@@ -333,280 +389,161 @@ class BaseSnackbar(MDCard):
Whether to use automatic closing of the snackbar or not.
:attr:`auto_dismiss` is a :class:`~kivy.properties.BooleanProperty`
- and defaults to `'True'`.
+ and defaults to `True`.
"""
- bg_color = ColorProperty(None)
+ radius = ListProperty([5, 5, 5, 5])
"""
- Snackbar background.
+ Snackbar radius.
- :attr:`bg_color` is a :class:`~kivy.properties.ColorProperty`
- and defaults to `None`.
+ :attr:`radius` is a :class:`~kivy.properties.ListProperty`
+ and defaults to `[5, 5, 5, 5]`
"""
- buttons = ListProperty()
+ bg_color = ColorProperty(None, deprecated=True)
"""
- Snackbar buttons.
+ Snackbar background color in (r, g, b, a) or string format.
- :attr:`buttons` is a :class:`~kivy.properties.ListProperty`
- and defaults to `'[]'`
+ .. deprecated:: 1.2.0
+ Use 'md_bg_color` instead.
+
+ :attr:`bg_color` is a :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
"""
- radius = ListProperty([5, 5, 5, 5])
+ buttons = ListProperty(deprecated=True)
"""
- Snackbar radius.
+ Snackbar buttons.
- :attr:`radius` is a :class:`~kivy.properties.ListProperty`
- and defaults to `'[5, 5, 5, 5]'`
+ .. deprecated:: 1.2.0
+
+ :attr:`buttons` is a :class:`~kivy.properties.ListProperty`
+ and defaults to `[]`
"""
snackbar_animation_dir = OptionProperty(
"Bottom",
options=["Top", "Bottom", "Left", "Right"],
+ deprecated=True,
)
"""
Snackbar animation direction.
+ Available options are: `'Top'`, `'Bottom'`, `'Left'`, `'Right'`.
- Available options are: `"Top"`, `"Bottom"`, `"Left"`, `"Right"`
+ .. deprecated:: 1.2.0
:attr:`snackbar_animation_dir` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'Bottom'`.
"""
- snackbar_x = NumericProperty("0dp")
+ snackbar_x = NumericProperty(0, deprecated=True)
"""
The snackbar x position in the screen
+ .. deprecated:: 1.2.0
+
:attr:`snackbar_x` is a :class:`~kivy.properties.NumericProperty`
- and defaults to `0dp`.
+ and defaults to `0`.
"""
- snackbar_y = NumericProperty("0dp")
+ snackbar_y = NumericProperty(0, deprecated=True)
"""
The snackbar x position in the screen
+ .. deprecated:: 1.2.0
+
:attr:`snackbar_y` is a :class:`~kivy.properties.NumericProperty`
- and defaults to `0dp`.
+ and defaults to `0`.
"""
- _interval = 0
-
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
self.register_event_type("on_open")
self.register_event_type("on_dismiss")
+ self.opacity = 0
- def dismiss(self, *args):
+ def dismiss(self, *args) -> None:
"""Dismiss the snackbar."""
- def dismiss(interval):
- if self.snackbar_animation_dir == "Top":
- anim = Animation(y=(Window.height + self.height), d=0.2)
- elif self.snackbar_animation_dir == "Left":
- anim = Animation(x=-self.width, d=0.2)
- elif self.snackbar_animation_dir == "Right":
- anim = Animation(x=Window.width, d=0.2)
- else:
- anim = Animation(y=-self.height, d=0.2)
-
- anim.bind(
- on_complete=lambda *args: Window.parent.remove_widget(self)
- )
- anim.start(self)
-
- Clock.schedule_once(dismiss, 0.5)
- self.dispatch("on_dismiss")
-
- def open(self):
- """Show the snackbar."""
+ super().on_dismiss()
- def wait_interval(interval):
- self._interval += interval
- if self._interval > self.duration:
- self.dismiss()
- Clock.unschedule(wait_interval)
- self._interval = 0
+ def open(self) -> None:
+ """Show the snackbar."""
- for c in Window.parent.children:
- if isinstance(c, BaseSnackbar):
+ for widget in Window.parent.children:
+ if widget.__class__ is MDSnackbar:
return
- if self.snackbar_y > (Window.height - self.height):
- self.snackbar_y = Window.height - self.height
-
- self._calc_radius()
-
- if self.size_hint_x == 1:
- self.size_hint_x = (Window.width - self.snackbar_x) / Window.width
-
- if (
- self.snackbar_animation_dir == "Top"
- or self.snackbar_animation_dir == "Bottom"
- ):
- self.x = self.snackbar_x
-
- if self.snackbar_animation_dir == "Top":
- self.y = Window.height + self.height
- else:
- self.y = -self.height
-
- Window.parent.add_widget(self)
-
- if self.snackbar_animation_dir == "Top":
- anim = Animation(
- y=self.snackbar_y
- if self.snackbar_y != 0
- else Window.height - self.height,
- d=0.2,
+ Window.parent.add_widget(self)
+ super().on_open()
+
+ def add_widget(self, widget, *args, **kwargs):
+ def check_color(color):
+ if not widget.text_color:
+ widget.theme_text_color = "Custom"
+ widget.text_color = color
+
+ if isinstance(widget, MDSnackbarCloseButton):
+ widget.icon_size = "20sp"
+ check_color("white")
+ self.ids.close_container.add_widget(widget)
+ if len(self.ids.close_container.children) >= 2:
+ Logger.warning(
+ "KivyMD: "
+ "Do not use more than one button to close the snackbar. "
+ "This is contrary to the material design rules "
+ "of version 3"
)
- else:
- anim = Animation(
- y=self.snackbar_y if self.snackbar_y != 0 else 0, d=0.2
+ if isinstance(widget, MDSnackbarActionButton):
+ self.ids.action_container.add_widget(widget)
+ check_color(self.theme_cls.primary_color)
+ if len(self.ids.action_container.children) >= 2:
+ Logger.warning(
+ "KivyMD: "
+ "Do not use more than one action button. "
+ "This is contrary to the material design rules "
+ "of version 3"
)
-
- elif (
- self.snackbar_animation_dir == "Left"
- or self.snackbar_animation_dir == "Right"
- ):
- self.y = self.snackbar_y
-
- if self.snackbar_animation_dir == "Left":
- self.x = -Window.width
- else:
- self.x = Window.width
-
- Window.parent.add_widget(self)
- anim = Animation(
- x=self.snackbar_x if self.snackbar_x != 0 else 0, d=0.2
- )
-
- if self.auto_dismiss:
- anim.bind(
- on_complete=lambda *args: Clock.schedule_interval(
- wait_interval, 0
+ if isinstance(widget, MDLabel):
+ widget.adaptive_height = True
+ widget.pos_hint = {"center_y": 0.5}
+ check_color("white")
+ self.ids.label_container.add_widget(widget)
+ if len(self.ids.label_container.children) >= 4:
+ Logger.warning(
+ "KivyMD: "
+ "Do not use more than three lines in the snackbar. "
+ "This is contrary to the material design rules "
+ "of version 3"
)
- )
- anim.start(self)
- self.dispatch("on_open")
-
- def on_open(self, *args):
- """Called when a dialog is opened."""
-
- def on_dismiss(self, *args):
- """Called when the dialog is closed."""
-
- def on_buttons(self, instance, value):
- def on_buttons(interval):
- for button in value:
- if issubclass(button.__class__, (BaseButton,)):
- self.add_widget(button)
- else:
- raise ValueError(
- f"The {button} object must be inherited from the base class <BaseButton>"
- )
-
- Clock.schedule_once(on_buttons)
-
- def _calc_radius(self):
- if (
- self.snackbar_animation_dir == "Top"
- or self.snackbar_animation_dir == "Bottom"
- ):
-
- if self.snackbar_y == 0 and self.snackbar_x == 0:
-
- if self.size_hint_x == 1:
- self.radius = [0, 0, 0, 0]
- else:
- if self.snackbar_animation_dir == "Top":
- self.radius = [0, 0, self.radius[2], 0]
- else:
- self.radius = [0, self.radius[1], 0, 0]
-
- elif self.snackbar_y != 0 and self.snackbar_x == 0:
-
- if self.size_hint_x == 1:
- self.radius = [0, 0, 0, 0]
- else:
- if self.snackbar_y >= Window.height - self.height:
- self.radius = [0, 0, self.radius[2], 0]
- else:
- self.radius = [0, self.radius[1], self.radius[2], 0]
-
- elif self.snackbar_y == 0 and self.snackbar_x != 0:
-
- if self.size_hint_x == 1:
- if self.snackbar_animation_dir == "Top":
- self.radius = [0, 0, 0, self.radius[3]]
- else:
- self.radius = [self.radius[0], 0, 0, 0]
- else:
- if self.snackbar_animation_dir == "Top":
- self.radius = [0, 0, self.radius[2], self.radius[3]]
- else:
- self.radius = [self.radius[0], self.radius[1], 0, 0]
-
- else: # self.snackbar_y != 0 and self.snackbar_x != 0
-
- if self.size_hint_x == 1:
- self.radius = [self.radius[0], 0, 0, self.radius[3]]
- elif self.snackbar_y >= Window.height - self.height:
- self.radius = [0, 0, self.radius[2], self.radius[3]]
-
- elif (
- self.snackbar_animation_dir == "Left"
- or self.snackbar_animation_dir == "Right"
+ elif isinstance(
+ widget,
+ (
+ SnackbarLabelContainer,
+ SnackbarActionButtonContainer,
+ SnackbarCloseButtonContainer,
+ ),
):
+ return super().add_widget(widget)
- if self.snackbar_y == 0 and self.snackbar_x == 0:
-
- if self.size_hint_x == 1:
- self.radius = [0, 0, 0, 0]
- else:
- self.radius = [0, self.radius[1], 0, 0]
-
- elif self.snackbar_y != 0 and self.snackbar_x == 0:
-
- if self.size_hint_x == 1:
- self.radius = [0, 0, 0, 0]
- else:
- self.radius = [0, self.radius[1], self.radius[2], 0]
-
- elif self.snackbar_y == 0 and self.snackbar_x != 0:
+ def on_open(self, *args) -> None:
+ """Called when a snackbar opened."""
- if self.size_hint_x == 1:
- self.radius = [self.radius[0], 0, 0, 0]
- else:
- self.radius = [self.radius[0], self.radius[1], 0, 0]
+ def on_dismiss(self, *args) -> None:
+ """Called when a snackbar closed."""
- else: # self.snackbar_y != 0 and self.snackbar_x != 0
- if self.size_hint_x == 1:
- if self.snackbar_y >= Window.height - self.height:
- self.radius = [0, 0, 0, self.radius[3]]
- else:
- self.radius = [self.radius[0], 0, 0, self.radius[3]]
- elif self.snackbar_y >= Window.height - self.height:
- self.radius = [0, 0, self.radius[2], self.radius[3]]
-
-
-class Snackbar(BaseSnackbar):
+class Snackbar(MDSnackbar):
"""
- Snackbar inherits all its functionality from `BaseSnackbar`
+ .. deprecated:: 1.2.0
+ Use :class:`~kivymd.uix.snackbar.MDSnackbar`
+ class instead.
"""
- text = StringProperty()
- """
- The text that will appear in the snackbar.
-
- :attr:`text` is a :class:`~kivy.properties.StringProperty`
- and defaults to `''`.
- """
-
- font_size = NumericProperty("15sp")
- """
- The font size of the text that will appear in the snackbar.
-
- :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and
- defaults to `'15sp'`.
- """
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ Logger.warning(
+ "KivyMD: "
+ "The `Snackbar` class has been deprecated. "
+ "Use the `MDSnackbar` class instead."
+ )

diff --git a/sbapp/kivymd/uix/spinner/spinner.py b/sbapp/kivymd/uix/spinner/spinner.py
index fd6b214f..6bf3c330 100755
--- a/sbapp/kivymd/uix/spinner/spinner.py
+++ b/sbapp/kivymd/uix/spinner/spinner.py
@@ -138,6 +138,10 @@ class MDSpinner(ThemableBehavior, Widget):
:class:`MDSpinner` is an implementation of the circular progress
indicator in `Google's Material Design`.
+ For more information, see in the
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivy.uix.widget.Widget` classes documentation.
+
It can be used either as an indeterminate indicator that loops while
the user waits for something to happen, or as a determinate indicator.
@@ -184,7 +188,7 @@ class MDSpinner(ThemableBehavior, Widget):
color = ColorProperty(None, allownone=True)
"""
- Spinner color.
+ Spinner color in (r, g, b, a) or string format.
:attr:`color` is a :class:`~kivy.properties.ColorProperty`
and defaults to `[0, 0, 0, 0]`.

diff --git a/sbapp/kivymd/uix/stacklayout.py b/sbapp/kivymd/uix/stacklayout.py
index 8988e8b3..c5028ca5 100644
--- a/sbapp/kivymd/uix/stacklayout.py
+++ b/sbapp/kivymd/uix/stacklayout.py
@@ -87,11 +87,14 @@ __all__ = ("MDStackLayout",)
from kivy.uix.stacklayout import StackLayout
+from kivymd.theming import ThemableBehavior
from kivymd.uix import MDAdaptiveWidget
from kivymd.uix.behaviors import DeclarativeBehavior
-class MDStackLayout(DeclarativeBehavior, StackLayout, MDAdaptiveWidget):
+class MDStackLayout(
+ DeclarativeBehavior, ThemableBehavior, StackLayout, MDAdaptiveWidget
+):
"""
Stack layout class. For more information, see in the
:class:`~kivy.uix.stacklayout.StackLayout` class documentation.

diff --git a/sbapp/kivymd/uix/swiper/swiper.py b/sbapp/kivymd/uix/swiper/swiper.py
index b89e2e54..e169c919 100644
--- a/sbapp/kivymd/uix/swiper/swiper.py
+++ b/sbapp/kivymd/uix/swiper/swiper.py
@@ -250,8 +250,10 @@ class _ItemsBox(AnchorLayout):
class MDSwiperItem(MDBoxLayout):
"""
- :class:`MDSwiperItem` is a :class:`BoxLayout` but it's size is adjusted
- automatically.
+ Swiper item class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation.
"""
_root = ObjectProperty()
@@ -293,6 +295,13 @@ class MDSwiperItem(MDBoxLayout):
class MDSwiper(MDScrollView):
+ """
+ Swiper class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.scrollview.MDScrollView` class documentation.
+ """
+
items_spacing = NumericProperty("20dp")
"""
The space between each :class:`MDSwiperItem`.
@@ -506,7 +515,6 @@ class MDSwiper(MDScrollView):
self.dispatch("on_swipe_right")
def on_scroll_start(self, touch, check_children=True):
-
if platform in ["ios", "android"]:
return super().on_scroll_start(touch)

diff --git a/sbapp/kivymd/uix/tab/tab.py b/sbapp/kivymd/uix/tab/tab.py
index 816711e9..4e97befd 100755
--- a/sbapp/kivymd/uix/tab/tab.py
+++ b/sbapp/kivymd/uix/tab/tab.py
@@ -1412,8 +1412,16 @@ class MDTabs(
AnchorLayout,
):
"""
+ Tabs class.
You can use this class to create your own tabbed panel.
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivymd.uix.behaviors.SpecificBackgroundColorBehavior` and
+ :class:`~kivy.uix.anchorlayout.AnchorLayout`
+ classes documentation.
+
:Events:
`on_tab_switch`
Called when switching tabs.
@@ -1518,7 +1526,7 @@ class MDTabs(
background_color = ColorProperty(None)
"""
- Background color of tabs in ``rgba`` format.
+ Background color of tabs in (r, g, b, a) or string format.
:attr:`background_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
@@ -1526,7 +1534,7 @@ class MDTabs(
underline_color = ColorProperty([0, 0, 0, 0])
"""
- Underline color of tabs in ``rgba`` format.
+ Underline color of tabs in (r, g, b, a) or string format.
:attr:`underline_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[0, 0, 0, 0]`.
@@ -1534,7 +1542,7 @@ class MDTabs(
text_color_normal = ColorProperty(None)
"""
- Text color of the label when it is not selected.
+ Text color in (r, g, b, a) or string format of the label when it is not selected.
:attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
@@ -1542,7 +1550,7 @@ class MDTabs(
text_color_active = ColorProperty(None)
"""
- Text color of the label when it is selected.
+ Text color in (r, g, b, a) or string format of the label when it is selected.
:attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
@@ -1592,7 +1600,7 @@ class MDTabs(
indicator_color = ColorProperty(None)
"""
- Color indicator in ``rgba`` format.
+ Color indicator in (r, g, b, a) or string format.
:attr:`indicator_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.

diff --git a/sbapp/kivymd/uix/textfield/textfield.kv b/sbapp/kivymd/uix/textfield/textfield.kv
index 6f11abb1..1ed7597e 100644
--- a/sbapp/kivymd/uix/textfield/textfield.kv
+++ b/sbapp/kivymd/uix/textfield/textfield.kv
@@ -7,6 +7,7 @@
# "round" mode.
Color:
+ group: "round-color"
rgba: self._fill_color if self.mode == "round" else (0, 0, 0, 0)
Ellipse:
angle_start: 180
@@ -19,49 +20,32 @@
pos: (self.width - dp(18)) + self.x - self.height / 2.0, self.y
size: self.height, self.height
Rectangle:
- pos: self.x + dp(9), self.y
- size: self.width - dp(18), self.height
+ pos: self.x + dp(14), self.y
+ size: self.width - dp(28), self.height
Color:
rgba:
( \
(self.line_color_focus if not self.error else self.error_color) \
- if self.focus \
- else self.theme_cls.disabled_hint_text_color \
+ if self.focus else ( \
+ self.theme_cls.disabled_hint_text_color \
+ if not self.line_color_normal else \
+ self.line_color_normal) \
) \
if self.mode == "round" else \
(0, 0, 0, 0)
- Line:
- points:
- self.x + dp(18), \
- self.y, \
- self.x + self.width - dp(18), \
- self.y
- Line:
- points:
- self.x + dp(18), \
- self.y + self.height, \
- self.x + self.width - dp(18), \
- self.y + self.height
- Line:
- ellipse:
- self.x - self.height / 2 + dp(18), \
- self.y, \
- self.height, \
- self.height, \
- 180, \
- 360
- Line:
- ellipse:
- self.width + self.x - self.height / 2.0 - dp(18), \
+ SmoothLine:
+ width: dp(1)
+ rounded_rectangle:
+ self.x, \
self.y, \
+ self.width, \
self.height, \
- self.height, \
- 360, \
- 540
+ self.height / 2
# "fill" mode.
Color:
+ group: "fill-color"
rgba: self._fill_color if self.mode == "fill" else (0, 0, 0, 0)
RoundedRectangle:
pos: self.x, self.y
@@ -70,6 +54,7 @@
# Static underline texture.
Color:
+ group: "static-underline-color"
rgba:
(self._line_color_normal \
if self.line_color_normal else self.theme_cls.divider_color) \
@@ -82,6 +67,7 @@
# Active underline (on focus) texture.
Color:
+ group: "active-underline-color"
rgba:
self._line_color_focus \
if self.mode in ("line", "fill") and self.active_line \
@@ -94,7 +80,10 @@
# Helper text texture.
Color:
+ group: "helper-text-color"
rgba:
+ self.theme_cls.disabled_hint_text_color \
+ if self.disabled else \
self._helper_text_color
Rectangle:
texture: self._helper_text_label.texture
@@ -106,7 +95,11 @@
# Right/left icon texture.
Color:
- rgba: self._icon_right_color if self.icon_right else self._icon_left_color
+ group: "right-left-icons-color"
+ rgba:
+ self.theme_cls.disabled_hint_text_color \
+ if self.disabled else \
+ (self._icon_right_color if self.icon_right else self._icon_left_color)
Rectangle:
texture:
self._icon_right_label.texture if self.icon_right else self._icon_left_label.texture
@@ -137,7 +130,11 @@
# Max length texture.
Color:
- rgba: self._max_length_text_color
+ group: "max-length-color"
+ rgba:
+ self.theme_cls.disabled_hint_text_color \
+ if self.disabled else \
+ self._max_length_text_color
Rectangle:
texture: self._max_length_label.texture
size: self._max_length_label.texture_size
@@ -148,16 +145,87 @@
# Cursor blink.
Color:
rgba:
- (self.text_color_focus if self.focus else self._text_color_normal) \
+ ( \
+ (self.text_color_focus if not self.error else self.error_color) \
+ if self.focus \
+ else self._text_color_normal \
+ ) \
if self.focus and not self._cursor_blink \
- else (0, 0, 0, 0)
+ else \
+ (0, 0, 0, 0)
Rectangle:
pos: (int(x) for x in self.cursor_pos)
size: 1, -self.line_height
+ # "rectangle" mode
+ Color:
+ group: "rectangle-color"
+ rgba:
+ ( \
+ (self.line_color_focus if not self.error else self.error_color) \
+ if self.focus else \
+ self.line_color_normal \
+ ) \
+ if self.mode == "rectangle" else \
+ (0, 0, 0, 0)
+ SmoothLine:
+ width: dp(1)
+ rounded_rectangle:
+ self.x, \
+ self.y, \
+ self.width, \
+ self.height - self._hint_text_label.texture_size[1] // 2, \
+ root.radius[0]
+
+ # The background color line of the widget on which the text field
+ # is placed (for background hint text texture).
+ Color:
+ rgba:
+ ( \
+ ( \
+ self.parent.md_bg_color \
+ if hasattr(self.parent, "md_bg_color") \
+ and self.parent.md_bg_color != [1, 1, 1, 0] else \
+ self.theme_cls.bg_normal \
+ ) \
+ if self.focus else \
+ ( \
+ (0, 0, 0, 0) if not self.text else \
+ ( \
+ self.parent.md_bg_color \
+ if hasattr(self.parent, "md_bg_color") \
+ and self.parent.md_bg_color != [1, 1, 1, 0] else \
+ self.theme_cls.bg_normal \
+ ) \
+ ) \
+ ) \
+ if self.mode == "rectangle" else \
+ (0, 0, 0, 0)
+ SmoothLine:
+ width: dp(2)
+ points:
+ self.x + dp(10), \
+ self.top - self._hint_text_label.texture_size[1] // 2, \
+ self.x + dp(16) + self._hint_text_label.texture_size[0], \
+ self.top - self._hint_text_label.texture_size[1] // 2
+
+ # Text color.
+ Color:
+ group: "text-color"
+ rgba:
+ self.theme_cls.disabled_hint_text_color if self.disabled else \
+ ( \
+ self.text_color_focus if self.focus else self._text_color_normal
+ ) \
+ if not self.error else self.error_color
+
+ canvas.after:
# Hint text texture.
Color:
+ group: "hint-text-color"
rgba:
+ self.theme_cls.disabled_hint_text_color \
+ if self.disabled else \
self._hint_text_color
Rectangle:
texture: self._hint_text_label.texture
@@ -179,34 +247,6 @@
if self.mode != "line" else \
dp(-6)) if self.mode != "rectangle" else dp(-4)) - self._hint_y
- # "rectangle" mode
- Color:
- rgba:
- (self.line_color_focus if not self.error else self.error_color) \
- if self.focus else \
- self.line_color_normal
- Line:
- width: dp(1) if self.mode == "rectangle" else dp(0.00001)
- points:
- (
- self.x + self._line_blank_space_right_point,
- self.top - self._hint_text_label.texture_size[1] // 2,
- self.right, self.top - self._hint_text_label.texture_size[1] // 2,
- self.right, self.y,
- self.x, self.y,
- self.x, self.top - self._hint_text_label.texture_size[1] // 2,
- self.x + self._line_blank_space_left_point,
- self.top - self._hint_text_label.texture_size[1] // 2
- )
-
- # Text color.
- Color:
- rgba:
- self.disabled_foreground_color if self.disabled else \
- ( \
- self.text_color_focus if self.focus else self._text_color_normal
- ) \
- if not self.error else self.error_color
font_name: "Roboto" if not self.font_name else self.font_name
foreground_color: self.theme_cls.text_color

diff --git a/sbapp/kivymd/uix/textfield/textfield.py b/sbapp/kivymd/uix/textfield/textfield.py
index 08ea932e..a351580f 100755
--- a/sbapp/kivymd/uix/textfield/textfield.py
+++ b/sbapp/kivymd/uix/textfield/textfield.py
@@ -528,6 +528,15 @@ class Validator:
class MDTextFieldRect(ThemableBehavior, TextInput):
+ """
+ Textfield rect class.
+
+ For more information, see in the
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivy.uix.textinput.TextInput`
+ classes documentation.
+ """
+
line_anim = BooleanProperty(True)
"""
If True, then text field shows animated line when on focus.
@@ -606,6 +615,18 @@ class MDTextField(
Validator,
AutoFormatTelephoneNumber,
):
+ """
+ Textfield class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivy.uix.textinput.TextInput` and
+ :class:`~Validator` and
+ :class:`~AutoFormatTelephoneNumber`
+ classes documentation.
+ """
+
helper_text = StringProperty()
"""
Text for ``helper_text`` mode.
@@ -1189,7 +1210,7 @@ class MDTextField(
radius = ListProperty([10, 10, 0, 0])
"""
- The corner radius for a text field in `fill` mode.
+ The corner radius for a text field in `fill/rectangle` mode.
:attr:`radius` is a :class:`~kivy.properties.ListProperty` and
defaults to `[10, 10, 0, 0]`.
@@ -1277,6 +1298,7 @@ class MDTextField(
_hint_text_font_size=self._hint_text_label.setter("font_size"),
_icon_right_color=self._icon_right_label.setter("text_color"),
_icon_left_color=self._icon_left_label.setter("text_color"),
+ font_name_hint_text=self._hint_text_label.setter("font_name"),
text=self.set_text,
)
self.theme_cls.bind(
@@ -1536,7 +1558,7 @@ class MDTextField(
if self.mode == "rectangle":
self.set_notch_rectangle()
- if not self.text and not self.focus:
+ if (not self.text and not self.focus) or (self.text and not self.focus):
self.on_focus(instance_text_field, False)
if self.mode == "round" and self.text:
@@ -1588,9 +1610,15 @@ class MDTextField(
self.helper_text_mode in ("on_focus", "persistent")
and self.helper_text
):
- self.set_helper_text_color(self.helper_text_color_focus)
+ Clock.schedule_once(
+ lambda x: self.set_helper_text_color(
+ self.helper_text_color_focus
+ )
+ )
if self.mode == "fill":
- self.set_fill_color(self.fill_color_focus)
+ Clock.schedule_once(
+ lambda x: self.set_fill_color(self.fill_color_focus)
+ )
self.set_active_underline_width(self.width)
self.set_pos_hint_text(
@@ -1598,30 +1626,62 @@ class MDTextField(
if self.mode != "rectangle"
else dp(10)
)
- self.set_hint_text_color(focus)
+ Clock.schedule_once(lambda x: self.set_hint_text_color(focus))
self.set_hint_text_font_size(sp(12))
if self.max_text_length:
- self.set_max_length_text_color(self.max_length_text_color)
+ Clock.schedule_once(
+ lambda x: self.set_max_length_text_color(
+ self.max_length_text_color
+ )
+ )
if self.icon_right:
- self.set_icon_right_color(self.icon_right_color_focus)
+ Clock.schedule_once(
+ lambda x: self.set_icon_right_color(
+ self.icon_right_color_focus
+ )
+ )
if self.icon_left:
- self.set_icon_left_color(self.icon_left_color_focus)
+ Clock.schedule_once(
+ lambda x: self.set_icon_left_color(
+ self.icon_left_color_focus
+ )
+ )
if self.error:
if self.hint_text:
- self.set_hint_text_color(focus, self.error)
+ Clock.schedule_once(
+ lambda x: self.set_hint_text_color(focus, self.error)
+ )
if self.helper_text:
- self.set_helper_text_color(self.error_color)
+ Clock.schedule_once(
+ lambda x: self.set_helper_text_color(self.error_color)
+ )
if self.max_text_length:
- self.set_max_length_text_color(self.error_color)
+ Clock.schedule_once(
+ lambda x: self.set_max_length_text_color(
+ self.error_color
+ )
+ )
if self.icon_right:
- self.set_icon_right_color(self.error_color)
+ Clock.schedule_once(
+ lambda x: self.set_icon_right_color(self.error_color)
+ )
if self.icon_left:
- self.set_icon_left_color(self.error_color)
+ Clock.schedule_once(
+ lambda x: self.set_icon_left_color(self.error_color)
+ )
else:
if self.helper_text_mode == "persistent" and self.helper_text:
- self.set_helper_text_color(self.helper_text_color_normal)
+ Clock.schedule_once(
+ lambda x: self.set_helper_text_color(
+ self.helper_text_color_normal
+ )
+ )
+ if self.helper_text_mode == "on_focus" and self.helper_text:
+ Clock.schedule_once(
+ lambda x: self.set_helper_text_color([0.0, 0.0, 0.0, 0.0])
+ )
if self.mode == "rectangle" and not self.text:
self.set_notch_rectangle(joining=True)
if not self.text:
@@ -1634,24 +1694,42 @@ class MDTextField(
self.set_pos_hint_text(y)
self.set_hint_text_font_size(sp(16))
- if self.icon_right:
- self.set_icon_right_color(self.icon_right_color_normal)
- if self.icon_left:
- self.set_icon_left_color(self.icon_left_color_normal)
+ if self.icon_right and not self.error:
+ Clock.schedule_once(
+ lambda x: self.set_icon_right_color(
+ self.icon_right_color_normal
+ )
+ )
+ if self.icon_left and not self.error:
+ Clock.schedule_once(
+ lambda x: self.set_icon_left_color(
+ self.icon_left_color_normal
+ )
+ )
if self.hint_text:
- self.set_hint_text_color(focus, self.error)
+ Clock.schedule_once(
+ lambda x: self.set_hint_text_color(focus, self.error)
+ )
self.set_active_underline_width(0)
- self.set_max_length_text_color([0, 0, 0, 0])
+ Clock.schedule_once(
+ lambda x: self.set_max_length_text_color([0, 0, 0, 0])
+ )
if self.mode == "fill":
- self.set_fill_color(self.fill_color_normal)
+ Clock.schedule_once(
+ lambda x: self.set_fill_color(self.fill_color_normal)
+ )
self.error = self._get_has_error() or self.error
if self.error:
self.set_static_underline_color(self.error_color)
else:
- self.set_static_underline_color(self.line_color_normal)
+ Clock.schedule_once(
+ lambda x: self.set_static_underline_color(
+ self.line_color_normal
+ )
+ )
def on_icon_left(self, instance_text_field, icon_name: str) -> None:
self._icon_left_label.icon = icon_name
@@ -1669,33 +1747,61 @@ class MDTextField(
"""
if error:
- self.set_max_length_text_color(self.error_color)
+ Clock.schedule_once(
+ lambda x: self.set_max_length_text_color(self.error_color)
+ )
self.set_active_underline_color(self.error_color)
if self.hint_text:
self.set_hint_text_color(self.focus, self.error)
if self.helper_text:
- self.set_helper_text_color(self.error_color)
+ Clock.schedule_once(
+ lambda x: self.set_helper_text_color(self.error_color)
+ )
if self.icon_right:
- self.set_icon_right_color(self.error_color)
+ Clock.schedule_once(
+ lambda x: self.set_icon_right_color(self.error_color)
+ )
if self.icon_left:
- self.set_icon_left_color(self.error_color)
+ Clock.schedule_once(
+ lambda x: self.set_icon_left_color(self.error_color)
+ )
if self.helper_text_mode == "on_error":
- self.set_helper_text_color(self.error_color)
+ Clock.schedule_once(
+ lambda x: self.set_helper_text_color(self.error_color)
+ )
else:
- self.set_max_length_text_color(self.max_length_text_color)
+ Clock.schedule_once(
+ lambda x: self.set_max_length_text_color(
+ self.max_length_text_color
+ )
+ )
self.set_active_underline_color(self.line_color_focus)
if self.hint_text:
self.set_hint_text_color(self.focus)
if self.helper_text:
- self.set_helper_text_color(self.helper_text_color_focus)
+ Clock.schedule_once(
+ lambda x: self.set_helper_text_color(
+ self.helper_text_color_focus
+ )
+ )
if self.icon_right:
- self.set_icon_right_color(self.icon_right_color_focus)
+ Clock.schedule_once(
+ lambda x: self.set_icon_right_color(
+ self.icon_right_color_focus
+ )
+ )
if self.icon_left:
- self.set_icon_left_color(self.icon_left_color_focus)
- if self.helper_text_mode in ("on_focus", "on_error"):
- self.set_helper_text_color([0, 0, 0, 0])
- elif self.helper_text_mode == "persistent":
- self.set_helper_text_color(self.helper_text_color_normal)
+ Clock.schedule_once(
+ lambda x: self.set_icon_left_color(
+ self.icon_left_color_focus
+ )
+ )
+ if self.helper_text_mode == "persistent":
+ Clock.schedule_once(
+ lambda x: self.set_helper_text_color(
+ self.helper_text_color_normal
+ )
+ )
def on_hint_text(self, instance_text_field, hint_text: str) -> None:
if hint_text:
@@ -1715,32 +1821,32 @@ class MDTextField(
def on_text_color_normal(
self, instance_text_field, color: Union[list, str]
- ):
+ ) -> None:
self._text_color_normal = color
def on_hint_text_color_normal(
self, instance_text_field, color: Union[list, str]
- ):
+ ) -> None:
self._hint_text_color = color
def on_helper_text_color_normal(
self, instance_text_field, color: Union[list, str]
- ):
+ ) -> None:
self._helper_text_color = color
def on_icon_right_color_normal(
self, instance_text_field, color: Union[list, str]
- ):
+ ) -> None:
self._icon_right_color = color
def on_line_color_normal(
self, instance_text_field, color: Union[list, str]
- ):
+ ) -> None:
self._line_color_normal = color
def on_max_length_text_color(
self, instance_text_field, color: Union[list, str]
- ):
+ ) -> None:
self._max_length_text_color = color
def _set_color(self, attr_name: str, color: str, updated: bool) -> None:
@@ -1798,79 +1904,75 @@ class MDTextField(
if __name__ == "__main__":
- from kivy.core.window import Window
from kivy.lang import Builder
from kivy.uix.textinput import TextInput
- Window.size = (800, 750)
-
from kivymd.app import MDApp
KV = """
MDScreen:
- MDBoxLayout:
- id: box
- orientation: "vertical"
- spacing: "20dp"
- adaptive_height: True
- size_hint_x: .8
- pos_hint: {"center_x": .5, "center_y": .5}
-
- MDTextField:
- hint_text: "Label"
- helper_text: "Error message"
- mode: "rectangle"
- max_text_length: 5
-
- MDTextField:
- icon_left: "git"
- hint_text: "Label"
- helper_text: "Error message"
- mode: "rectangle"
-
- MDTextField:
- icon_left: "git"
- hint_text: "Label"
- helper_text: "Error message"
- mode: "fill"
-
- MDTextField:
- hint_text: "Label"
- helper_text: "Error message"
- mode: "fill"
-
- MDTextField:
- hint_text: "Label"
- helper_text: "Error message"
-
- MDTextField:
- icon_left: "git"
- hint_text: "Label"
- helper_text: "Error message"
-
- MDTextField:
- hint_text: "Round mode"
- mode: "round"
- max_text_length: 15
- helper_text: "Message"
-
- MDTextField:
- hint_text: "Date dd/mm/yyyy in [01/01/1900, 01/01/2100] interval"
- helper_text: "Enter a valid dd/mm/yyyy date"
- validator: "date"
- date_format: "dd/mm/yyyy"
- date_interval: "01/01/1900", "01/01/2100"
-
- MDTextField:
- hint_text: "Email"
- helper_text: "user@gmail.com"
- validator: "email"
-
- MDFlatButton:
- text: "SET TEXT"
- pos_hint: {"center_x": .5}
- on_release: app.set_text()
+ MDScrollView:
+
+ MDList:
+ id: box
+ spacing: "32dp"
+ padding: "56dp", "12dp", "56dp", "12dp"
+
+ MDTextField:
+ hint_text: "Label"
+ helper_text: "Error message"
+ mode: "rectangle"
+ max_text_length: 5
+
+ MDTextField:
+ icon_left: "git"
+ hint_text: "Label"
+ helper_text: "Error message"
+ mode: "rectangle"
+
+ MDTextField:
+ icon_left: "git"
+ hint_text: "Label"
+ helper_text: "Error message"
+ mode: "fill"
+
+ MDTextField:
+ hint_text: "Label"
+ helper_text: "Error message"
+ mode: "fill"
+
+ MDTextField:
+ hint_text: "Label"
+ helper_text: "Error message"
+
+ MDTextField:
+ icon_left: "git"
+ hint_text: "Label"
+ helper_text: "Error message"
+
+ MDTextField:
+ hint_text: "Round mode"
+ mode: "round"
+ max_text_length: 15
+ helper_text: "Message"
+
+ MDTextField:
+ hint_text: "Date dd/mm/yyyy in [01/01/1900, 01/01/2100] interval"
+ helper_text: "Enter a valid dd/mm/yyyy date"
+ validator: "date"
+ date_format: "dd/mm/yyyy"
+ date_interval: "01/01/1900", "01/01/2100"
+
+ MDTextField:
+ hint_text: "Email"
+ helper_text: "user@gmail.com"
+ validator: "email"
+
+ MDFlatButton:
+ text: "SET TEXT"
+ pos_hint: {"center_x": .5}
+ on_release: app.set_text()
"""
class Test(MDApp):

diff --git a/sbapp/kivymd/uix/toolbar/__init__.py b/sbapp/kivymd/uix/toolbar/__init__.py
index d6fa24da..e4f163e9 100644
--- a/sbapp/kivymd/uix/toolbar/__init__.py
+++ b/sbapp/kivymd/uix/toolbar/__init__.py
@@ -1,2 +1,8 @@
# NOQA F401
-from .toolbar import MDBottomAppBar, MDTopAppBar
+from .toolbar import (
+ MDActionBottomAppBarButton,
+ MDActionOverFlowButton,
+ MDBottomAppBar,
+ MDFabBottomAppBarButton,
+ MDTopAppBar,
+)

diff --git a/sbapp/kivymd/uix/toolbar/toolbar.py b/sbapp/kivymd/uix/toolbar/toolbar.py
index 0a48548f..091b0408 100755
--- a/sbapp/kivymd/uix/toolbar/toolbar.py
+++ b/sbapp/kivymd/uix/toolbar/toolbar.py
@@ -8,19 +8,21 @@ Components/Toolbar
`Material Design spec, App bars: bottom <https://material.io/components/app-bars-bottom/app-bars-bottom.html>`_
- `Material Design 3 spec, App bars: bottom <https://m3.material.io/components/top-app-bar/overview>`_
+ `Material Design 3 spec, App bars: top <https://m3.material.io/components/top-app-bar/overview>`_
+
+ `Material Design 3 spec, App bars: bottom <https://m3.material.io/components/bottom-app-bar/overview>`_
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/app-bar-top.png
:align: center
-`KivyMD` provides the following toolbar positions for use:
+`KivyMD` provides the following bar positions for use:
-- Top_
-- Bottom_
+- TopAppBar_
+- BottomAppBar_
-.. Top:
-Top
----
+.. TopAppBar_:
+TopAppBar
+---------
.. code-block:: python
@@ -31,6 +33,7 @@ Top
KV = '''
MDBoxLayout:
orientation: "vertical"
+ md_bg_color: "#1E1E15"
MDTopAppBar:
title: "MDTopAppBar"
@@ -41,12 +44,14 @@ Top
'''
- class Test(MDApp):
+ class Example(MDApp):
def build(self):
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
return Builder.load_string(KV)
- Test().run()
+ Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-1.png
:align: center
@@ -58,6 +63,7 @@ Add left menu
MDTopAppBar:
title: "MDTopAppBar"
+ anchor_title: "left"
left_action_items: [["menu", lambda x: app.callback()]]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-2.png
@@ -74,6 +80,7 @@ Add right menu
MDTopAppBar:
title: "MDTopAppBar"
+ anchor_title: "left"
right_action_items: [["dots-vertical", lambda x: app.callback()]]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-3.png
@@ -86,31 +93,38 @@ Add two item to the right menu
MDTopAppBar:
title: "MDTopAppBar"
- right_action_items: [["dots-vertical", lambda x: app.callback_1()], ["clock", lambda x: app.callback_2()]]
+ anchor_title: "left"
+ right_action_items:
+ [
+ ["dots-vertical", lambda x: app.callback_1()],
+ ["clock", lambda x: app.callback_2()]
+ ]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-4.png
:align: center
-Change toolbar color
---------------------
+Change bar color
+----------------
.. code-block:: kv
MDTopAppBar:
title: "MDTopAppBar"
- md_bg_color: app.theme_cls.accent_color
+ anchor_title: "left"
+ md_bg_color: "brown"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-5.png
:align: center
-Change toolbar text color
--------------------------
+Change bar text color
+---------------------
.. code-block:: kv
MDTopAppBar:
title: "MDTopAppBar"
- specific_text_color: app.theme_cls.accent_color
+ anchor_title: "left"
+ specific_text_color: "white"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-6.png
:align: center
@@ -122,14 +136,19 @@ Shadow elevation control
MDTopAppBar:
title: "Elevation 4"
+ anchor_title: "left"
elevation: 4
+ shadow_color: "brown"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-7.png
:align: center
-.. Bottom:
-Bottom
-------
+.. BottomAppBar:
+BottomAppBar
+------------
+
+M2 style bottom app bar
+-----------------------
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/app-bar-bottom.png
:align: center
@@ -145,24 +164,28 @@ Usage
KV = '''
MDBoxLayout:
+ md_bg_color: "#1E1E15"
# Will always be at the bottom of the screen.
MDBottomAppBar:
MDTopAppBar:
- title: "Title"
+ title: "MDBottomAppBar"
icon: "git"
type: "bottom"
left_action_items: [["menu", lambda x: x]]
'''
- class Test(MDApp):
+ class Example(MDApp):
def build(self):
+ self.theme_cls.material_style = "M2"
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
return Builder.load_string(KV)
- Test().run()
+ Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-8.png
:align: center
@@ -177,7 +200,7 @@ Event ``on_action_button``:
MDBottomAppBar:
MDTopAppBar:
- title: "Title"
+ title: "MDBottomAppBar"
icon: "git"
type: "bottom"
left_action_items: [["menu", lambda x: x]]
@@ -198,7 +221,7 @@ Mode:
MDBottomAppBar:
MDTopAppBar:
- title: "Title"
+ title: "MDBottomAppBar"
icon: "git"
type: "bottom"
left_action_items: [["menu", lambda x: x]]
@@ -212,7 +235,7 @@ Mode:
MDBottomAppBar:
MDTopAppBar:
- title: "Title"
+ title: "MDBottomAppBar"
icon: "git"
type: "bottom"
left_action_items: [["menu", lambda x: x]]
@@ -227,22 +250,306 @@ Custom color
.. code-block:: kv
MDBottomAppBar:
- md_bg_color: 0, 1, 0, 1
MDTopAppBar:
- title: "Title"
+ title: "MDBottomAppBar"
icon: "git"
type: "bottom"
left_action_items: [["menu", lambda x: x]]
icon_color: 0, 1, 0, 1
+ md_bg_bottom_color: "brown"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-11.png
:align: center
+M3 style bottom app bar
+-----------------------
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/app-bar-bottom-m3.png
+ :align: center
+
+.. code-block:: python
+
+ from kivy.lang import Builder
+
+ from kivymd.app import MDApp
+
+ KV = '''
+ MDFloatLayout:
+ md_bg_color: "#151511"
+
+ MDBottomAppBar:
+ md_bg_color: "#232217"
+ icon_color: "#8A8D79"
+
+ MDFabBottomAppBarButton:
+ icon: "plus"
+ md_bg_color: "#373A22"
+ '''
+
+
+ class Example(MDApp):
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ return Builder.load_string(KV)
+
+
+ Example().run()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-1.png
+ :align: center
+
+Add action items
+----------------
+
+.. code-block:: kv
+
+ #:import MDActionBottomAppBarButton kivymd.uix.toolbar.MDActionBottomAppBarButton
+
+
+ MDFloatLayout:
+
+ MDBottomAppBar:
+ action_items:
+ [
+ MDActionBottomAppBarButton(icon="gmail"),
+ MDActionBottomAppBarButton(icon="label-outline"),
+ MDActionBottomAppBarButton(icon="bookmark"),
+ ]
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-2.png
+ :align: center
+
+Change action items
+-------------------
+
+.. code-block:: python
+
+ from kivy.lang import Builder
+
+ from kivymd.app import MDApp
+
+ KV = '''
+ #:import MDActionBottomAppBarButton kivymd.uix.toolbar.MDActionBottomAppBarButton
+
+
+ MDFloatLayout:
+ md_bg_color: "#151511"
+
+ MDBottomAppBar:
+ id: bottom_appbar
+ md_bg_color: "#232217"
+ icon_color: "#8A8D79"
+ action_items:
+ [
+ MDActionBottomAppBarButton(icon="gmail"),
+ MDActionBottomAppBarButton(icon="bookmark"),
+ ]
+
+ MDFabBottomAppBarButton:
+ icon: "plus"
+ md_bg_color: "#373A22"
+ on_release: app.change_actions_items()
+ '''
+
+
+ class Example(MDApp):
+ def change_actions_items(self):
+ self.root.ids.bottom_appbar.action_items = [
+ MDActionBottomAppBarButton(icon="magnify"),
+ MDActionBottomAppBarButton(icon="trash-can-outline"),
+ MDActionBottomAppBarButton(icon="download-box-outline"),
+ ]
+
+ def build(self):
+ self.theme_cls.theme_style = "Dark"
+ return Builder.load_string(KV)
+
+
+ Example().run()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-3.gif
+ :align: center
+
+A practical example
+-------------------
+
+.. code-block:: python
+
+ from kivy.clock import Clock
+ from kivy.lang import Builder
+ from kivy.properties import StringProperty, BooleanProperty, ObjectProperty
+ from kivy.uix.behaviors import FocusBehavior
+ from kivy.uix.recycleboxlayout import RecycleBoxLayout
+ from kivy.uix.recycleview.layout import LayoutSelectionBehavior
+ from kivy.uix.recycleview.views import RecycleDataViewBehavior
+
+ from kivymd.uix.boxlayout import MDBoxLayout
+ from kivymd.uix.toolbar import MDActionBottomAppBarButton
+ from kivymd.app import MDApp
+ from kivymd.utils import asynckivy
+
+ from faker import Faker # pip install Faker
+
+ KV = '''
+ #:import MDFabBottomAppBarButton kivymd.uix.toolbar.MDFabBottomAppBarButton
+
+
+ <UserCard>
+ orientation: "vertical"
+ adaptive_height: True
+ md_bg_color: "#373A22" if self.selected else "#1F1E15"
+ radius: 16
+ padding: 0, 0, 0, "16dp"
+
+ TwoLineAvatarListItem:
+ divider: None
+ _no_ripple_effect: True
+ text: root.name
+ secondary_text: root.time
+ theme_text_color: "Custom"
+ text_color: "#8A8D79"
+ secondary_theme_text_color: self.theme_text_color
+ secondary_text_color: self.text_color
+
+ ImageLeftWidget:
+ source: root.avatar
+ radius: self.height / 2
+
+ MDLabel:
+ text: root.text
+ adaptive_height: True
+ theme_text_color: "Custom"
+ text_color: "#8A8D79"
+ padding_x: "16dp"
+ shorten: True
+ shorten_from: "right"
+
+ Widget:
+
+
+ MDFloatLayout:
+ md_bg_color: "#151511"
+
+ RecycleView:
+ id: card_list
+ viewclass: "UserCard"
+
+ SelectableRecycleGridLayout:
+ orientation: 'vertical'
+ spacing: "16dp"
+ padding: "16dp"
+ default_size: None, dp(120)
+ default_size_hint: 1, None
+ size_hint_y: None
+ height: self.minimum_height
+ multiselect: True
+ touch_multiselect: True
+
+ MDBottomAppBar:
+ id: bottom_appbar
+ scroll_cls: card_list
+ allow_hidden: True
+ md_bg_color: "#232217"
+ icon_color: "#8A8D79"
+
+ MDFabBottomAppBarButton:
+ id: fab_button
+ icon: "plus"
+ md_bg_color: "#373A22"
+ '''
+
+
+ class UserCard(RecycleDataViewBehavior, MDBoxLayout):
+ name = StringProperty()
+ time = StringProperty()
+ text = StringProperty()
+ avatar = StringProperty()
+ callback = ObjectProperty(lambda x: x)
+
+ index = None
+ selected = BooleanProperty(False)
+ selectable = BooleanProperty(True)
+
+ def refresh_view_attrs(self, rv, index, data):
+ self.index = index
+ return super().refresh_view_attrs(rv, index, data)
+
+ def on_touch_down(self, touch):
+ if super().on_touch_down(touch):
+ return True
+ if self.collide_point(*touch.pos) and self.selectable:
+ Clock.schedule_once(self.callback)
+ return self.parent.select_with_touch(self.index, touch)
+
+ def apply_selection(self, rv, index, is_selected):
+ self.selected = is_selected
+ rv.data[index]["selected"] = is_selected
+
+
+ class SelectableRecycleGridLayout(
+ FocusBehavior, LayoutSelectionBehavior, RecycleBoxLayout
+ ):
+ pass
+
+
+ class Test(MDApp):
+ selected_cards = False
+
+ def build(self):
+ return Builder.load_string(KV)
+
+ def on_tap_card(self, *args):
+ datas = [data["selected"] for data in self.root.ids.card_list.data]
+ if True in datas and not self.selected_cards:
+ self.root.ids.bottom_appbar.action_items = [
+ MDActionBottomAppBarButton(icon="gmail"),
+ MDActionBottomAppBarButton(icon="label-outline"),
+ MDActionBottomAppBarButton(icon="bookmark"),
+ ]
+ self.root.ids.fab_button.icon = "pencil"
+ self.selected_cards = True
+ else:
+ if len(list(set(datas))) == 1 and not list(set(datas))[0]:
+ self.selected_cards = False
+ if not self.selected_cards:
+ self.root.ids.bottom_appbar.action_items = [
+ MDActionBottomAppBarButton(icon="magnify"),
+ MDActionBottomAppBarButton(icon="trash-can-outline"),
+ MDActionBottomAppBarButton(icon="download-box-outline"),
+ ]
+ self.root.ids.fab_button.icon = "plus"
+
+ def on_start(self):
+ async def generate_card():
+ for i in range(10):
+ await asynckivy.sleep(0)
+ self.root.ids.card_list.data.append(
+ {
+ "name": fake.name(),
+ "time": fake.date(),
+ "avatar": fake.image_url(),
+ "text": fake.text(),
+ "selected": False,
+ "callback": self.on_tap_card,
+ }
+ )
+
+ self.on_tap_card()
+ fake = Faker()
+ Clock.schedule_once(lambda x: asynckivy.start(generate_card()))
+
+
+ Test().run()
+
+.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-4.gif
+ :align: center
+
Tooltips
--------
-You can add MDTooltips to the Toolbar icons by ading a text string to the toolbar item, as shown below
+You can add MDTooltips to the icons by adding a text string to the bar item,
+as shown below:
.. code-block:: python
@@ -259,7 +566,13 @@ You can add MDTooltips to the Toolbar icons by ading a text string to the toolba
title: "MDTopAppBar"
left_action_items: [["menu", "This is the navigation"]]
right_action_items:
- [["dots-vertical", lambda x: app.callback(x), "this is the More Actions"]]
+ [
+ [
+ "dots-vertical",
+ lambda x: app.callback(x),
+ "this is the More Actions"
+ ]
+ ]
MDLabel:
text: "Content"
@@ -267,17 +580,21 @@ You can add MDTooltips to the Toolbar icons by ading a text string to the toolba
'''
- class Test(MDApp):
+ class Example(MDApp):
def build(self):
+ self.theme_cls.material_style = "M2"
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
return Builder.load_string(KV)
def callback(self, button):
Snackbar(text="Hello World").open()
- Test().run()
-Material design 3 style
------------------------
+ Example().run()
+
+M3 style top app bar
+--------------------
.. code-block:: python
@@ -298,9 +615,10 @@ Material design 3 style
'''
- class TestNavigationDrawer(MDApp):
+ class Example(MDApp):
def build(self):
- self.theme_cls.material_style = "M3"
+ self.theme_cls.theme_style = "Dark"
+ self.theme_cls.primary_palette = "Orange"
return Builder.load_string(KV)
def on_start(self):
@@ -309,30 +627,40 @@ Material design 3 style
MDTopAppBar(
type_height=type_height,
headline_text=f"Headline {type_height.lower()}",
- md_bg_color="#2d2734",
+ md_bg_color="brown",
left_action_items=[["arrow-left", lambda x: x]],
right_action_items=[
["attachment", lambda x: x],
["calendar", lambda x: x],
["dots-vertical", lambda x: x],
],
- title="Title" if type_height == "small" else ""
+ title="Title" if type_height == "small" else "",
+ anchor_title="left",
)
)
- TestNavigationDrawer().run()
+ Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-m3.png
:align: center
"""
-__all__ = ("MDTopAppBar", "MDBottomAppBar", "ActionTopAppBarButton")
+from __future__ import annotations
+
+__all__ = (
+ "MDTopAppBar",
+ "MDBottomAppBar",
+ "MDActionBottomAppBarButton",
+ "MDFabBottomAppBarButton",
+ "MDActionOverFlowButton",
+)
import os
from math import cos, radians, sin
from typing import Union
+from kivy import Logger
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.core.window import Window
@@ -349,13 +677,16 @@ from kivy.properties import (
)
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
+from kivy.uix.scrollview import ScrollView
from kivymd import uix_path
from kivymd.color_definitions import text_colors
+from kivymd.material_resources import TOP_APP_BAR_ELEVATION
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import (
CommonElevationBehavior,
DeclarativeBehavior,
+ RotateBehavior,
ScaleBehavior,
SpecificBackgroundColorBehavior,
)
@@ -364,6 +695,7 @@ from kivymd.uix.controllers import WindowController
from kivymd.uix.list import OneLineIconListItem
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.tooltip import MDTooltip
+from kivymd.utils import asynckivy
from kivymd.utils.set_bars_colors import set_bars_colors
with open(
@@ -372,22 +704,63 @@ with open(
Builder.load_string(kv_file.read())
-class ActionBottomAppBarButton(MDFloatingActionButton, ScaleBehavior):
+class MDFabBottomAppBarButton(
+ MDFloatingActionButton, RotateBehavior, ScaleBehavior, MDTooltip
+):
"""
- Implements a floating action button (FAB) for a toolbar with type 'bottom'.
+ Implements a floating action button (FAB) for a bar with type 'bottom'.
+
+ For more information, see in the
+ :class:`~kivymd.uix.button.MDFloatingActionButton` and
+ :class:`~kivymd.uix.behaviors.RotateBehavior` and
+ :class:`~kivymd.uix.behaviors.ScaleBehavior` and
+ :class:`~kivymd.uix.tooltip.MDTooltip`
+ classes documentation.
"""
+ def set__radius(self, *args) -> None:
+ super().set__radius()
+ if self.theme_cls.material_style == "M3":
+ self.elevation = 0
+
class ActionTopAppBarButton(MDIconButton, MDTooltip):
- """Implements action buttons on the toolbar."""
+ """
+ Implements action buttons on the bar.
+
+ For more information, see in the
+ :class:`~kivymd.uix.button.MDIconButton` and
+ :class:`~kivymd.uix.tooltip.MDTooltip`
+ classes documentation.
+ """
# The text of the menu item of the corresponding action button that will
# be displayed in the `OverFlowMenu` menu.
overflow_text = StringProperty()
-class ActionOverFlowButton(ActionTopAppBarButton):
- """Implements a toolbar action button for the `OverFlowMenu` menu."""
+class MDActionBottomAppBarButton(ActionTopAppBarButton):
+ """
+ Implements action buttons for a :class:'MDBottomAppBar' class.
+
+ .. versionadded:: 1.2.0
+
+ For more information, see in the
+ :class:`~kivymd.uix.button.MDIconButton` and
+ :class:`~kivymd.uix.tooltip.MDTooltip`
+ classes documentation.
+ """
+
+
+class MDActionOverFlowButton(ActionTopAppBarButton):
+ """
+ Implements a bar action button for the `OverFlowMenu` menu.
+
+ For more information, see in the
+ :class:`~kivymd.uix.button.MDIconButton` and
+ :class:`~kivymd.uix.tooltip.MDTooltip`
+ classes documentation.
+ """
icon = "dots-vertical"
@@ -411,7 +784,7 @@ class NotchedBox(
SpecificBackgroundColorBehavior,
BoxLayout,
):
- elevation = NumericProperty(4)
+ elevation = NumericProperty(TOP_APP_BAR_ELEVATION)
notch_radius = NumericProperty()
notch_center_x = NumericProperty("100dp")
@@ -540,7 +913,6 @@ class NotchedBox(
raise Exception("Invalid value for start angle")
for degree in range(start_angle, end_angle, step):
-
angle = radians(degree)
x = center[0] + (radius * cos(angle))
y = center[1] + (radius * sin(angle))
@@ -556,6 +928,14 @@ class NotchedBox(
class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
"""
+ Top app bar class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and
+ :class:`~NotchedBox` and
+ :class:`~kivymd.uix.controllers.WindowController`
+ classes documentation.
+
:Events:
`on_action_button`
Method for the button used for the :class:`~MDBottomAppBar` class.
@@ -563,13 +943,14 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
left_action_items = ListProperty()
"""
- The icons on the left of the toolbar.
+ The icons on the left of the bar.
To add one, append a list like the following:
.. code-block:: kv
MDTopAppBar:
- left_action_items: ["dots-vertical", callback, "tooltip text", "overflow text"]
+ left_action_items:
+ ["dots-vertical", callback, "tooltip text", "overflow text"]
``icon_name`` - is a string that corresponds to an icon definition:
@@ -578,9 +959,6 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
MDTopAppBar:
right_action_items: [["home"]]
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-icon.png
- :align: center
-
``callback`` - is the function called on a touch release event and:
.. code-block:: kv
@@ -607,7 +985,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
["message-reply", lambda x: app.callback(x), "Message reply"],
]
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-tooltip-text.gif
+ .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-tooltip-text.png
:align: center
``overflow text`` - is the text for menu items (:class:`~OverFlowMenuItem`)
@@ -616,18 +994,35 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
.. code-block:: kv
MDTopAppBar:
+ use_overflow: True
right_action_items:
[
- ["home", lambda x: app.callback(x), "", "Home"],
- ["message-star", lambda x: app.callback(x), "", "Message star"],
- ["message-question", lambda x: app.callback(x), "" , "Message question"],
- ["message-reply", lambda x: app.callback(x), "", "Message reply"],
+ ["home", lambda x: x, "", "Home"],
+ ["message-star", lambda x: x, "", "Message star"],
+ ["message-question", lambda x: x, "" , "Message question"],
+ ["message-reply", lambda x: x, "", "Message reply"],
]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-overflow-text.png
:align: center
- Both the ``callback`` and ``tooltip text`` and ``overflow text`` are
+ ``icon color`` - icon color:
+
+ .. code-block:: kv
+
+ MDTopAppBar:
+ right_action_items:
+ [
+ [
+ "dots-vertical",
+ callback,
+ "tooltip text",
+ "overflow text",
+ (1, 1, 1, 1),
+ ]
+ ]
+
+ Both the ``callback`` and ``tooltip text`` and ``overflow text`` and ``icon color`` are
optional but the order must be preserved.
:attr:`left_action_items` is an :class:`~kivy.properties.ListProperty`
@@ -636,7 +1031,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
right_action_items = ListProperty()
"""
- The icons on the left of the toolbar.
+ The icons on the left of the bar.
Works the same way as :attr:`left_action_items`.
:attr:`right_action_items` is an :class:`~kivy.properties.ListProperty`
@@ -645,7 +1040,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
title = StringProperty()
"""
- Text toolbar.
+ Text app bar.
.. code-block:: kv
@@ -755,7 +1150,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
.. code-block:: kv
MDTopAppBar:
- opposite_colors: True
+ opposite_colors: False
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-opposite-false.png
:align: center
@@ -763,7 +1158,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
md_bg_bottom_color = ColorProperty(None)
"""
- The background color in (r, g, b, a) format for the toolbar with the
+ The background color in (r, g, b, a) or string format for the bar with the
``bottom`` mode.
.. versionadded:: 1.0.0
@@ -773,7 +1168,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
MDBottomAppBar:
MDTopAppBar:
- md_bg_bottom_color: 0, 1, 0, 1
+ md_bg_bottom_color: "brown"
icon_color: self.md_bg_bottom_color
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-md-bg-bottom-color.png
@@ -786,11 +1181,11 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
set_bars_color = BooleanProperty(False)
"""
If `True` the background color of the bar status will be set automatically
- according to the current color of the toolbar.
+ according to the current color of the bar.
.. versionadded:: 1.0.0
- See `set_bars_colors <https://kivymd.readthedocs.io/en/latest/api/kivymd/utils/set_bars_colors/>`
+ See `set_bars_colors <https://kivymd.readthedocs.io/en/latest/api/kivymd/utils/set_bars_colors/>`_
for more information.
:attr:`set_bars_color` is an :class:`~kivy.properties.BooleanProperty`
@@ -811,15 +1206,12 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
use_overflow: True
right_action_items:
[
- ["home", lambda x: app.callback(x), "Home", "Home"],
- ["message-star", lambda x: app.callback(x), "Message star", "Message star"],
- ["message-question", lambda x: app.callback(x), "Message question", "Message question"],
- ["message-reply", lambda x: app.callback(x), "Message reply", "Message reply"],
+ ["home", lambda x: x, "Home", "Home"],
+ ["message-star", lambda x: x, "Message star", "Message star"],
+ ["message-question", lambda x: x, "Message question", "Message question"],
+ ["message-reply", lambda x: x, "Message reply", "Message reply"],
]
- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-use-overflow.gif
- :align: center
-
:attr:`use_overflow` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
@@ -852,10 +1244,10 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
overflow_cls: CustomOverFlowMenu()
right_action_items:
[
- ["home", lambda x: app.callback(x), "Home", "Home"],
- ["message-star", lambda x: app.callback(x), "Message star", "Message star"],
- ["message-question", lambda x: app.callback(x), "Message question", "Message question"],
- ["message-reply", lambda x: app.callback(x), "Message reply", "Message reply"],
+ ["home", lambda x: x, "Home", "Home"],
+ ["message-star", lambda x: x, "Message star", "Message star"],
+ ["message-question", lambda x: x, "Message question", "Message question"],
+ ["message-reply", lambda x: x, "Message reply", "Message reply"],
]
MDLabel:
@@ -869,7 +1261,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
pass
- class Test(MDApp):
+ class Example(MDApp):
def build(self):
return Builder.load_string(KV)
@@ -877,7 +1269,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
print(instance_action_top_appbar_button)
- Test().run()
+ Example().run()
:attr:`overflow_cls` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
@@ -895,7 +1287,8 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
icon_color = ColorProperty()
"""
- Color action button. Only for :class:`~MDBottomAppBar` class.
+ Color in (r, g, b, a) or string format action button. Only for
+ :class:`~MDBottomAppBar` class.
:attr:`icon_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[]`.
@@ -905,7 +1298,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
anchor_title = OptionProperty(None, options=["left", "center", "right"])
"""
- Position toolbar title. Only used with `material_style = 'M3'`
+ Position bar title. Only used with `material_style = 'M3'`
Available options are: `'left'`, `'center'`, `'right'`.
:attr:`anchor_title` is an :class:`~kivy.properties.OptionProperty`
@@ -914,7 +1307,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
headline_text = StringProperty()
"""
- Headline text toolbar.
+ Headline text bar.
.. versionadded:: 1.0.0
@@ -924,7 +1317,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
headline_text_color = ColorProperty(None)
"""
- Headline text color.
+ Headline text color in (r, g, b, a) or string format.
.. versionadded:: 1.0.0
@@ -934,11 +1327,11 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
type_height = OptionProperty("small", options=["medium", "large", "small"])
"""
- Toolbar height type.
+ Bar height type.
.. versionadded:: 1.0.0
- Available options are: 'small', 'large', 'small'.
+ Available options are: 'medium', 'large', 'small'.
:attr:`type_height` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'small'`.
@@ -951,7 +1344,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
_overflow_menu_items = []
def __init__(self, **kwargs):
- self.action_button = ActionBottomAppBarButton()
+ self.action_button = MDFabBottomAppBarButton()
super().__init__(**kwargs)
self.register_event_type("on_action_button")
@@ -987,7 +1380,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
def on_width(self, instance_toolbar, width: float) -> None:
"""
- Called when the toolbar is resized (size of the application window).
+ Called when the bar is resized (size of the application window).
"""
if self.mode == "center":
@@ -1022,7 +1415,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
self.remove_overflow_button()
def remove_overflow_button(self) -> None:
- """Removes an overflow button to the toolbar."""
+ """Removes an overflow button to the bar."""
if self.overflow_action_button_is_added():
action_overflow_button = self.ids.right_actions.children[0]
@@ -1030,10 +1423,10 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
self._overflow_menu_items = []
def add_overflow_button(self) -> None:
- """Adds an overflow button to the toolbar."""
+ """Adds an overflow button to the bar."""
self.ids.right_actions.add_widget(
- ActionOverFlowButton(
+ MDActionOverFlowButton(
theme_text_color="Custom"
if not self.opposite_colors
else "Primary",
@@ -1046,19 +1439,19 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
def overflow_action_button_is_added(self) -> bool:
"""
Returns `True` if at least one action button
- (:class:`~ActionTopAppBarButton') on the toolbar is added to the
+ (:class:`~ActionTopAppBarButton') on the bar is added to the
overflow.
"""
if (
not self.ids.right_actions.children[0].__class__
- is ActionOverFlowButton
+ is MDActionOverFlowButton
):
return False
return True
def add_action_button_to_overflow(self):
- """Adds an overflow button to the toolbar."""
+ """Adds an overflow button to the bar."""
if len(self.ids.right_actions.children) > 1:
button_to_be_added = self.ids.right_actions.children[1]
@@ -1318,21 +1711,28 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
if len(item) > 1 and not item[1]:
item[1] = lambda x: None
if len(item) == 2:
- if type(item[1]) is str:
+ if isinstance(item[1], str) or isinstance(item[1], tuple):
item.insert(1, lambda x: None)
else:
item.append("")
+ if len(item) == 3:
+ if isinstance(item[2], tuple):
+ item.insert(2, "")
instance_box_layout.add_widget(
ActionTopAppBarButton(
icon=item[0],
on_release=item[1],
tooltip_text=item[2],
- overflow_text=item[3] if len(item) == 4 else "",
+ overflow_text=item[3]
+ if (len(item) == 4 and isinstance(item[3], str))
+ else "",
theme_text_color="Custom"
if not self.opposite_colors
else "Primary",
- text_color=self.specific_text_color,
+ text_color=self.specific_text_color
+ if not (len(item) == 4 and isinstance(item[3], tuple))
+ else item[3],
opposite_colors=self.opposite_colors,
)
)
@@ -1366,20 +1766,460 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController):
][self.theme_cls.primary_hue]
-class MDBottomAppBar(DeclarativeBehavior, FloatLayout):
+class MDBottomAppBar(
+ DeclarativeBehavior,
+ ThemableBehavior,
+ SpecificBackgroundColorBehavior,
+ CommonElevationBehavior,
+ FloatLayout,
+):
+ """
+ Bottom app bar class.
+
+ For more information, see in the
+ :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivymd.uix.behaviors.SpecificBackgroundColorBehavior` and
+ :class:`~kivymd.uix.behaviors.CommonElevationBehavior` and
+ :class:`~kivy.uix.floatlayout.FloatLayout`
+ classes documentation.
+
+ :Events:
+ `on_show_bar`
+ The method is called when the :class:`~MDBottomAppBar` panel
+ is shown.
+ `on_hide_bar`
+ The method is called when the :class:`~MDBottomAppBar` panel
+ is hidden.
+ """
+
md_bg_color = ColorProperty([0, 0, 0, 0])
"""
- Color toolbar.
+ Color bar in (r, g, b, a) or string format.
:attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[0, 0, 0, 0]`.
"""
+ icon_color = ColorProperty(None)
+ """
+ Color bar in (r, g, b, a) or string format.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`icon_color` is an :class:`~kivy.properties.ColorProperty`
+ and defaults to `None`.
+ """
+
+ action_items = ListProperty()
+ """
+ The icons on the left bar.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`action_items` is an :class:`~kivy.properties.ListProperty`
+ and defaults to `[]`.
+ """
+
+ animation = BooleanProperty(True)
+ """
+ # TODO: add description.
+ # FIXME: changing the value does not affect anything.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`animation` is an :class:`~kivy.properties.BooleanProperty`
+ and defaults to `True`.
+ """
+
+ show_transition = StringProperty("linear")
+ """
+ Type of button display transition.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`show_transition` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `'linear'`.
+ """
+
+ hide_transition = StringProperty("in_back")
+ """
+ Type of button hidden transition.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`hide_transition` is a :class:`~kivy.properties.StringProperty`
+ and defaults to `'in_back'`.
+ """
+
+ hide_duration = NumericProperty(0.4)
+ """
+ Duration of button hidden transition.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`hide_duration` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `0.2`.
+ """
+
+ show_duration = NumericProperty(0.2)
+ """
+ Duration of button display transition.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`show_duration` is a :class:`~kivy.properties.NumericProperty`
+ and defaults to `0.2`.
+ """
+
+ scroll_cls = ObjectProperty()
+ """
+ Widget inherited from the :class:`~kivy.uix.scrollview.ScrollView` class.
+ The value must be set if the :attr:`allow_hidden` parameter is `True`.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`scroll_cls` is a :class:`~kivy.properties.ObjectProperty`
+ and defaults to `None`.
+ """
+
+ allow_hidden = BooleanProperty(False)
+ """
+ Allows or disables hiding the panel when scrolling content.
+ If the value is `True`, the :attr:`scroll_cls` parameter must be specified.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`allow_hidden` is a :class:`~kivy.properties.BooleanProperty`
+ and defaults to `False`.
+ """
+
+ bar_is_hidden = BooleanProperty(False)
+ """
+ Is the panel currently hidden.
+
+ .. versionadded:: 1.2.0
+
+ :attr:`bar_is_hidden` is a :class:`~kivy.properties.BooleanProperty`
+ and defaults to `False`.
+ """
+
+ _padding = dp(16)
+ _x = -dp(48)
+ _scroll_cls_y = 0
+ _cache = []
+ _current_data = []
+ _wait_removed = False
+ _animated_hidden = True
+ _animated_show = True
+ _fab_bottom_app_bar_button = None
+ _action_overflow_button = None
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.size_hint_y = None
+ if self.theme_cls.material_style == "M3":
+ self.register_event_type("on_show_bar")
+ self.register_event_type("on_hide_bar")
+
+ self.height = dp(80)
+ Clock.schedule_once(self.set_bg_color)
+
+ def button_centering_animation(
+ self,
+ button: MDActionOverFlowButton
+ | MDActionBottomAppBarButton
+ | MDFabBottomAppBarButton,
+ ) -> None:
+ """
+ Animation of centering buttons for
+ :class:`~MDActionOverFlowButton`,
+ :class:`~MDActionBottomAppBarButton` and
+ :class:`~MDFabBottomAppBarButton` classes.
+ """
+
+ if self.animation:
+ Animation(
+ y=self.height / 2 - dp(48) / 2,
+ opacity=1,
+ d=self.show_duration,
+ t=self.show_transition,
+ ).start(button)
+
+ def check_scroll_direction(self, scroll_cls, y: float) -> None:
+ """
+ Checks the scrolling direction.
+ Depending on the scrolling direction, hides or shows the
+ :class:`~MDBottomAppBar` panel.
+ """
+
+ if round(y, 1) < self._scroll_cls_y and not self.bar_is_hidden:
+ self.hide_bar()
+ if round(y, 1) > self._scroll_cls_y and self.bar_is_hidden:
+ self.show_bar()
+
+ self._scroll_cls_y = round(y, 1)
+
+ def show_bar(self) -> None:
+ """Show :class:`~MDBottomAppBar` panel."""
+
+ def on_complete(*args):
+ self.dispatch("on_show_bar")
+
+ def on_progress(animation, instance, progress):
+ if progress > 0.5 and self._animated_show:
+ self._animated_show = False
+ for i, widget in enumerate(self.children):
+ if isinstance(widget, MDActionBottomAppBarButton):
+ anim_icon = Animation(
+ y=self.height / 2 - dp(48) / 2,
+ d=self.show_duration,
+ t=self.show_transition,
+ )
+ Clock.schedule_once(
+ lambda x, y=widget: anim_icon.start(y),
+ i / 10,
+ )
+ if self._fab_bottom_app_bar_button:
+ Animation(
+ y=self._fab_bottom_app_bar_button.y + dp(4),
+ d=self.show_duration,
+ t=self.show_transition,
+ ).start(self._fab_bottom_app_bar_button)
+
+ self.bar_is_hidden = False
+ self._animated_show = True
+ anim = Animation(
+ y=0,
+ d=self.show_duration,
+ t=self.show_transition,
+ )
+ anim.bind(on_progress=on_progress, on_complete=on_complete)
+ anim.start(self)
+
+ def hide_bar(self) -> None:
+ """Hide :class:`~MDBottomAppBar` panel."""
+
+ def on_complete(*args):
+ self.dispatch("on_hide_bar")
+
+ def on_progress(animation, instance, progress):
+ if (
+ progress > 0.5
+ and self._animated_hidden
+ and widget_icon == instance.icon
+ ):
+ self._animated_hidden = False
+ anim_bar = Animation(
+ y=-self.height,
+ d=self.hide_duration,
+ # t=self.hide_transition,
+ )
+ anim_bar.bind(on_complete=on_complete)
+ anim_bar.start(self)
+
+ if self._fab_bottom_app_bar_button:
+ Animation(
+ y=self._fab_bottom_app_bar_button.y - dp(4),
+ d=self.hide_duration,
+ t=self.hide_transition,
+ ).start(self._fab_bottom_app_bar_button)
+
+ self.bar_is_hidden = True
+ self._animated_hidden = True
+ len_children = len(self.children)
+ widget_icon = ""
+
+ for i, widget in enumerate(self.children):
+ if isinstance(widget, MDActionBottomAppBarButton):
+ anim = Animation(
+ y=-widget.height,
+ d=self.hide_duration,
+ t=self.hide_transition,
+ )
+ if i + 2 == len_children:
+ widget_icon = widget.icon
+ anim.bind(on_progress=on_progress)
+ Clock.schedule_once(
+ lambda x, y=widget: anim.start(y),
+ i / 10,
+ )
+
+ def on_show_bar(self, *args) -> None:
+ """
+ The method is called when the :class:`~MDBottomAppBar` panel
+ is shown.
+ """
+
+ def on_hide_bar(self, *args) -> None:
+ """
+ The method is called when the :class:`~MDBottomAppBar` panel
+ is hidden.
+ """
+
+ def on_scroll_cls(self, instance, scroll_cls) -> None:
+ """
+ Called when the value of the :attr:`scroll_cls` attribute changes.
+ """
+
+ def on_scroll_cls(*args):
+ if not self.allow_hidden:
+ Logger.warning(
+ "KivyMD: "
+ "In order for the bottom bar to be automatically hidden "
+ "in addition to the `scroll_cls` parameter, set the value "
+ "of the `allow_hidden` parameter to `True`"
+ )
+
+ if issubclass(scroll_cls.__class__, ScrollView):
+ if self.allow_hidden:
+ scroll_cls.bind(scroll_y=self.check_scroll_direction)
+ else:
+ raise TypeError(
+ f"The `scroll_cls` parameter must be an object inherited from "
+ f"the {ScrollView} class"
+ )
+
+ if self.theme_cls.material_style == "M3":
+ Clock.schedule_once(on_scroll_cls)
+
+ def on_size(self, *args) -> None:
+ """Called when the root screen is resized."""
+
+ if (
+ self._fab_bottom_app_bar_button
+ and self.theme_cls.material_style == "M3"
+ ):
+ self._fab_bottom_app_bar_button.x = Window.width - (dp(56) + dp(16))
+
+ def on_action_items(self, instance, value: list) -> None:
+ """
+ Called when the value of the :attr:`action_items` attribute changes.
+ """
+
+ if self.theme_cls.material_style == "M2":
+ return
+
+ def wait_removed(*args):
+ if len(self.children) == 1 or not self.children:
+ Clock.unschedule(wait_removed)
+ self._wait_removed = False
+ self._x = -dp(48)
+ asynckivy.start(add_widget())
+
+ async def add_widget():
+ for button in value:
+ await asynckivy.sleep(0)
+ self.add_widget(button)
+
+ if self._cache:
+ self._cache.append(value)
+
+ for data in self._cache:
+ if value[0] in data:
+ for i, widget in enumerate(self.children):
+ if not self._wait_removed:
+ Clock.schedule_interval(wait_removed, 0)
+ self._wait_removed = True
+ if isinstance(widget, MDActionBottomAppBarButton):
+ anim = Animation(
+ y=-widget.height,
+ d=self.hide_duration,
+ t=self.hide_transition,
+ )
+ anim.bind(
+ on_complete=lambda x, y=widget: self.remove_widget(
+ y
+ )
+ )
+ Clock.schedule_once(
+ lambda x, y=widget: anim.start(y),
+ i / 10,
+ )
+ else:
+ self._cache.append(value)
+ self._current_data = value
+ asynckivy.start(add_widget())
+
+ def set_fab_opacity(self, *ars) -> None:
+ """
+ Sets the transparency value of the:class:`~MDFabBottomAppBarButton`
+ button.
+ """
+
+ self._fab_bottom_app_bar_button.ids.lbl_ic.opacity = 1
+
+ def set_fab_icon(self, instance, value) -> None:
+ """
+ Animates the size of the :class:`~MDFabBottomAppBarButton` button.
+ """
+
+ self._fab_bottom_app_bar_button.ids.lbl_ic.opacity = 0
+ anim = Animation(
+ scale_value_x=0,
+ scale_value_y=0,
+ opacity=0,
+ d=self.hide_duration,
+ t=self.hide_transition,
+ ) + Animation(
+ scale_value_x=1,
+ scale_value_y=1,
+ opacity=1,
+ d=self.show_duration,
+ t=self.show_transition,
+ )
+ anim.bind(on_complete=self.set_fab_opacity)
+ anim.start(instance)
+
+ def set_bg_color(self, *args) -> None:
+ """
+ Sets the background color for the :class:`~MDBottomAppBar` class.
+ """
+
+ if self.md_bg_color == [0, 0, 0, 0]:
+ self.md_bg_color = self.theme_cls.primary_color
+
+ def set_icon_color(
+ self, widget: MDActionOverFlowButton | MDActionBottomAppBarButton
+ ) -> None:
+ """
+ Sets the icon color for the :class:`~MDActionOverFlowButton` and
+ :class:`~MDActionBottomAppBarButton` classes.
+ """
+
+ if self.icon_color:
+ widget.theme_icon_color = "Custom"
+ widget.icon_color = self.icon_color
def add_widget(self, widget, index=0, canvas=None):
- if isinstance(widget, MDTopAppBar):
+ # For M2 style.
+ if (
+ isinstance(widget, MDTopAppBar)
+ and self.theme_cls.material_style == "M2"
+ ):
super().add_widget(widget)
+ widget.elevation = 0
return super().add_widget(widget.action_button)
+ # For M3 style.
+ if self.theme_cls.material_style == "M3":
+ if isinstance(widget, MDActionBottomAppBarButton):
+ self._x += widget.width
+ widget.pos = (
+ self._x + self._padding,
+ -dp(48) if self.animation else self.height / 2 - dp(48) / 2,
+ )
+ widget.opacity = int(not self.animation)
+ self.set_icon_color(widget)
+ super().add_widget(widget)
+ self.button_centering_animation(widget)
+ elif isinstance(widget, MDFabBottomAppBarButton):
+ widget.bind(icon=self.set_fab_icon)
+ self._fab_bottom_app_bar_button = widget
+ Clock.schedule_once(self.set_fab_opacity)
+ widget.scale_value_x = int(not self.animation)
+ widget.scale_value_y = int(not self.animation)
+ widget.pos = (
+ Window.width - (dp(56) + self._padding),
+ self.height / 2 - dp(56) / 2,
+ )
+ super().add_widget(widget)

diff --git a/sbapp/kivymd/uix/tooltip/tooltip.py b/sbapp/kivymd/uix/tooltip/tooltip.py
index 855119a5..ba6b5da0 100644
--- a/sbapp/kivymd/uix/tooltip/tooltip.py
+++ b/sbapp/kivymd/uix/tooltip/tooltip.py
@@ -97,9 +97,19 @@ with open(
class MDTooltip(ThemableBehavior, HoverBehavior, TouchBehavior):
+ """
+ Tooltip class.
+
+ For more information, see in the
+ :class:`~kivymd.theming.ThemableBehavior and
+ :class:`~kivymd.uix.behaviors.HoverBehavior` and
+ :class:`~kivymd.uix.behaviors.TouchBehavior`
+ classes documentation.
+ """
+
tooltip_bg_color = ColorProperty(None)
"""
- Tooltip background color in ``rgba`` format.
+ Tooltip background color in (r, g, b, a) or string format
:attr:`tooltip_bg_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
@@ -107,7 +117,7 @@ class MDTooltip(ThemableBehavior, HoverBehavior, TouchBehavior):
tooltip_text_color = ColorProperty(None)
"""
- Tooltip text color in ``rgba`` format.
+ Tooltip text color in (r, g, b, a) or string format
:attr:`tooltip_text_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
@@ -330,6 +340,15 @@ class MDTooltip(ThemableBehavior, HoverBehavior, TouchBehavior):
class MDTooltipViewClass(ThemableBehavior, BoxLayout):
+ """
+ Tooltip view class.
+
+ For more information, see in the
+ :class:`~kivymd.theming.ThemableBehavior` and
+ :class:`~kivy.uix.boxlayout.BoxLayout`
+ classes documentation.
+ """
+
tooltip_bg_color = ColorProperty(None)
"""
See :attr:`~MDTooltip.tooltip_bg_color`.

diff --git a/sbapp/kivymd/uix/transition/transition.py b/sbapp/kivymd/uix/transition/transition.py
index 169584d7..e905ea67 100644
--- a/sbapp/kivymd/uix/transition/transition.py
+++ b/sbapp/kivymd/uix/transition/transition.py
@@ -142,7 +142,6 @@ class MDTransitionBase(TransitionBase):
and self.manager.current_heroes
and self.screen_out.heroes_to
):
-
for heroes_tag in self.manager.current_heroes:
for hero_to_widget in self.screen_out.heroes_to:
if hero_to_widget.tag == heroes_tag:

diff --git a/sbapp/kivymd/uix/widget.py b/sbapp/kivymd/uix/widget.py
index 044722bb..27c1371e 100644
--- a/sbapp/kivymd/uix/widget.py
+++ b/sbapp/kivymd/uix/widget.py
@@ -38,11 +38,12 @@ __all__ = ("MDWidget",)
from kivy.uix.widget import Widget
+from kivymd.theming import ThemableBehavior
from kivymd.uix import MDAdaptiveWidget
from kivymd.uix.behaviors import DeclarativeBehavior
-class MDWidget(DeclarativeBehavior, MDAdaptiveWidget, Widget):
+class MDWidget(DeclarativeBehavior, ThemableBehavior, MDAdaptiveWidget, Widget):
"""
See :class:`~kivy.uix.Widget` class documentation for more information.

diff --git a/sbapp/kivymd/utils/fpsmonitor.py b/sbapp/kivymd/utils/fpsmonitor.py
index 2284ba85..4fbc2fca 100644
--- a/sbapp/kivymd/utils/fpsmonitor.py
+++ b/sbapp/kivymd/utils/fpsmonitor.py
@@ -11,7 +11,7 @@ application :
from kivy.clock import Clock
from kivy.lang import Builder
-from kivy.properties import NumericProperty, StringProperty
+from kivy.properties import NumericProperty, StringProperty, OptionProperty
from kivy.uix.label import Label
Builder.load_string(
@@ -20,7 +20,7 @@ Builder.load_string(
size_hint_y: None
height: self.texture_size[1]
text: root._fsp_value
- pos_hint: {"top": 1}
+ pos_hint: {root.anchor: 1}
canvas.before:
Color:
@@ -36,6 +36,9 @@ class FpsMonitor(Label):
updated_interval = NumericProperty(0.5)
"""FPS refresh rate."""
+ anchor = OptionProperty("top", options=["top", "bottom"])
+ """Monitor position."""
+
_fsp_value = StringProperty()
def start(self):

diff --git a/sbapp/main.py b/sbapp/main.py
index abd84a0c..7dc18912 100644
--- a/sbapp/main.py
+++ b/sbapp/main.py
@@ -1,6 +1,6 @@
__debug_build__ = False
-__disable_shaders__ = True
-__version__ = "0.5.2"
+__disable_shaders__ = False
+__version__ = "0.5.3"
__variant__ = "beta"
import sys

diff --git a/sbapp/patches/AndroidManifest.tmpl.xml b/sbapp/patches/AndroidManifest.tmpl.xml
index c29b290e..5a4f1284 100644
--- a/sbapp/patches/AndroidManifest.tmpl.xml
+++ b/sbapp/patches/AndroidManifest.tmpl.xml
@@ -31,11 +31,7 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
{% for perm in args.permissions %}
- {% if '.' in perm %}
- <uses-permission android:name="{{ perm }}" />
- {% else %}
- <uses-permission android:name="android.permission.{{ perm }}" />
- {% endif %}
+ <uses-permission android:name="{{ perm.name }}"{% if perm.maxSdkVersion %} android:maxSdkVersion="{{ perm.maxSdkVersion }}"{% endif %}{% if perm.usesPermissionFlags %} android:usesPermissionFlags="{{ perm.usesPermissionFlags }}"{% endif %} />
{% endfor %}
{% if args.wakelock %}

diff --git a/sbapp/patches/p4a_build.py b/sbapp/patches/p4a_build.py
new file mode 100644
index 00000000..66f1d9c2
--- /dev/null
+++ b/sbapp/patches/p4a_build.py
@@ -0,0 +1,1064 @@
+#!/usr/bin/env python3
+
+from gzip import GzipFile
+import hashlib
+import json
+from os.path import (
+ dirname, join, isfile, realpath,
+ relpath, split, exists, basename
+)
+from os import environ, listdir, makedirs, remove
+import os
+import shlex
+import shutil
+import subprocess
+import sys
+import tarfile
+import tempfile
+import time
+
+from distutils.version import LooseVersion
+from fnmatch import fnmatch
+import jinja2
+
+
+def get_dist_info_for(key, error_if_missing=True):
+ try:
+ with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh:
+ info = json.load(fileh)
+ value = info[key]
+ except (OSError, KeyError) as e:
+ if not error_if_missing:
+ return None
+ print("BUILD FAILURE: Couldn't extract the key `" + key + "` " +
+ "from dist_info.json: " + str(e))
+ sys.exit(1)
+ return value
+
+
+def get_hostpython():
+ return get_dist_info_for('hostpython')
+
+
+def get_bootstrap_name():
+ return get_dist_info_for('bootstrap')
+
+
+if os.name == 'nt':
+ ANDROID = 'android.bat'
+ ANT = 'ant.bat'
+else:
+ ANDROID = 'android'
+ ANT = 'ant'
+
+curdir = dirname(__file__)
+
+BLACKLIST_PATTERNS = [
+ # code versionning
+ '^*.hg/*',
+ '^*.git/*',
+ '^*.bzr/*',
+ '^*.svn/*',
+
+ # temp files
+ '~',
+ '*.bak',
+ '*.swp',
+
+ # Android artifacts
+ '*.apk',
+ '*.aab',
+]
+
+WHITELIST_PATTERNS = []
+
+if os.environ.get("P4A_BUILD_IS_RUNNING_UNITTESTS", "0") != "1":
+ PYTHON = get_hostpython()
+ _bootstrap_name = get_bootstrap_name()
+else:
+ PYTHON = "python3"
+ _bootstrap_name = "sdl2"
+
+if PYTHON is not None and not exists(PYTHON):
+ PYTHON = None
+
+if _bootstrap_name in ('sdl2', 'webview', 'service_only'):
+ WHITELIST_PATTERNS.append('pyconfig.h')
+
+environment = jinja2.Environment(loader=jinja2.FileSystemLoader(
+ join(curdir, 'templates')))
+
+
+DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS = 'org.kivy.android.PythonActivity'
+DEFAULT_PYTHON_SERVICE_JAVA_CLASS = 'org.kivy.android.PythonService'
+
+
+def ensure_dir(path):
+ if not exists(path):
+ makedirs(path)
+
+
+def render(template, dest, **kwargs):
+ '''Using jinja2, render `template` to the filename `dest`, supplying the
+
+ keyword arguments as template parameters.
+ '''
+
+ dest_dir = dirname(dest)
+ if dest_dir and not exists(dest_dir):
+ makedirs(dest_dir)
+
+ template = environment.get_template(template)
+ text = template.render(**kwargs)
+
+ f = open(dest, 'wb')
+ f.write(text.encode('utf-8'))
+ f.close()
+
+
+def is_whitelist(name):
+ return match_filename(WHITELIST_PATTERNS, name)
+
+
+def is_blacklist(name):
+ if is_whitelist(name):
+ return False
+ return match_filename(BLACKLIST_PATTERNS, name)
+
+
+def match_filename(pattern_list, name):
+ for pattern in pattern_list:
+ if pattern.startswith('^'):
+ pattern = pattern[1:]
+ else:
+ pattern = '*/' + pattern
+ if fnmatch(name, pattern):
+ return True
+
+
+def listfiles(d):
+ basedir = d
+ subdirlist = []
+ for item in os.listdir(d):
+ fn = join(d, item)
+ if isfile(fn):
+ yield fn
+ else:
+ subdirlist.append(join(basedir, item))
+ for subdir in subdirlist:
+ for fn in listfiles(subdir):
+ yield fn
+
+
+def make_tar(tfn, source_dirs, byte_compile_python=False, optimize_python=True):
+ '''
+ Make a zip file `fn` from the contents of source_dis.
+ '''
+
+ def clean(tinfo):
+ """cleaning function (for reproducible builds)"""
+ tinfo.uid = tinfo.gid = 0
+ tinfo.uname = tinfo.gname = ''
+ tinfo.mtime = 0
+ return tinfo
+
+ # get the files and relpath file of all the directory we asked for
+ files = []
+ for sd in source_dirs:
+ sd = realpath(sd)
+ for fn in listfiles(sd):
+ if is_blacklist(fn):
+ continue
+ if fn.endswith('.py') and byte_compile_python:
+ fn = compile_py_file(fn, optimize_python=optimize_python)
+ files.append((fn, relpath(realpath(fn), sd)))
+ files.sort() # deterministic
+
+ # create tar.gz of thoses files
+ gf = GzipFile(tfn, 'wb', mtime=0) # deterministic
+ tf = tarfile.open(None, 'w', gf, format=tarfile.USTAR_FORMAT)
+ dirs = []
+ for fn, afn in files:
+ dn = dirname(afn)
+ if dn not in dirs:
+ # create every dirs first if not exist yet
+ d = ''
+ for component in split(dn):
+ d = join(d, component)
+ if d.startswith('/'):
+ d = d[1:]
+ if d == '' or d in dirs:
+ continue
+ dirs.append(d)
+ tinfo = tarfile.TarInfo(d)
+ tinfo.type = tarfile.DIRTYPE
+ clean(tinfo)
+ tf.addfile(tinfo)
+
+ # put the file
+ tf.add(fn, afn, filter=clean)
+ tf.close()
+ gf.close()
+
+
+def compile_py_file(python_file, optimize_python=True):
+ '''
+ Compile python_file to *.pyc and return the filename of the *.pyc file.
+ '''
+
+ if PYTHON is None:
+ return
+
+ args = [PYTHON, '-m', 'compileall', '-b', '-f', python_file]
+ if optimize_python:
+ # -OO = strip docstrings
+ args.insert(1, '-OO')
+ return_code = subprocess.call(args)
+
+ if return_code != 0:
+ print('Error while running "{}"'.format(' '.join(args)))
+ print('This probably means one of your Python files has a syntax '
+ 'error, see logs above')
+ exit(1)
+
+ return ".".join([os.path.splitext(python_file)[0], "pyc"])
+
+
+def make_package(args):
+ # If no launcher is specified, require a main.py/main.pyc:
+ if (get_bootstrap_name() != "sdl" or args.launcher is None) and \
+ get_bootstrap_name() not in ["webview", "service_library"]:
+ # (webview doesn't need an entrypoint, apparently)
+ if args.private is None or (
+ not exists(join(realpath(args.private), 'main.py')) and
+ not exists(join(realpath(args.private), 'main.pyc'))):
+ print('''BUILD FAILURE: No main.py(c) found in your app directory. This
+file must exist to act as the entry point for you app. If your app is
+started by a file with a different name, rename it to main.py or add a
+main.py that loads it.''')
+ sys.exit(1)
+
+ assets_dir = "src/main/assets"
+
+ # Delete the old assets.
+ shutil.rmtree(assets_dir, ignore_errors=True)
+ ensure_dir(assets_dir)
+
+ # Add extra environment variable file into tar-able directory:
+ env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-")
+ with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f:
+ if hasattr(args, "window"):
+ f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n")
+ if hasattr(args, "sdl_orientation_hint"):
+ f.write("KIVY_ORIENTATION=" + str(args.sdl_orientation_hint) + "\n")
+ f.write("P4A_NUMERIC_VERSION=" + str(args.numeric_version) + "\n")
+ f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n")
+
+ # Package up the private data (public not supported).
+ use_setup_py = get_dist_info_for("use_setup_py",
+ error_if_missing=False) is True
+ private_tar_dirs = [env_vars_tarpath]
+ _temp_dirs_to_clean = []
+ try:
+ if args.private:
+ if not use_setup_py or (
+ not exists(join(args.private, "setup.py")) and
+ not exists(join(args.private, "pyproject.toml"))
+ ):
+ print('No setup.py/pyproject.toml used, copying '
+ 'full private data into .apk.')
+ private_tar_dirs.append(args.private)
+ else:
+ print("Copying main.py's ONLY, since other app data is "
+ "expected in site-packages.")
+ main_py_only_dir = tempfile.mkdtemp()
+ _temp_dirs_to_clean.append(main_py_only_dir)
+
+ # Check all main.py files we need to copy:
+ copy_paths = ["main.py", join("service", "main.py")]
+ for copy_path in copy_paths:
+ variants = [
+ copy_path,
+ copy_path.partition(".")[0] + ".pyc",
+ ]
+ # Check in all variants with all possible endings:
+ for variant in variants:
+ if exists(join(args.private, variant)):
+ # Make sure surrounding directly exists:
+ dir_path = os.path.dirname(variant)
+ if (len(dir_path) > 0 and
+ not exists(
+ join(main_py_only_dir, dir_path)
+ )):
+ os.mkdir(join(main_py_only_dir, dir_path))
+ # Copy actual file:
+ shutil.copyfile(
+ join(args.private, variant),
+ join(main_py_only_dir, variant),
+ )
+
+ # Append directory with all main.py's to result apk paths:
+ private_tar_dirs.append(main_py_only_dir)
+ if get_bootstrap_name() == "webview":
+ for asset in listdir('webview_includes'):
+ shutil.copy(join('webview_includes', asset), join(assets_dir, asset))
+
+ for asset in args.assets:
+ asset_src, asset_dest = asset.split(":")
+ if isfile(realpath(asset_src)):
+ ensure_dir(dirname(join(assets_dir, asset_dest)))
+ shutil.copy(realpath(asset_src), join(assets_dir, asset_dest))
+ else:
+ shutil.copytree(realpath(asset_src), join(assets_dir, asset_dest))
+
+ if args.private or args.launcher:
+ for arch in get_dist_info_for("archs"):
+ libs_dir = f"libs/{arch}"
+ make_tar(
+ join(libs_dir, "libpybundle.so"),
+ [f"_python_bundle__{arch}"],
+ byte_compile_python=args.byte_compile_python,
+ optimize_python=args.optimize_python,
+ )
+ make_tar(
+ join(assets_dir, "private.tar"),
+ private_tar_dirs,
+ byte_compile_python=args.byte_compile_python,
+ optimize_python=args.optimize_python,
+ )
+ finally:
+ for directory in _temp_dirs_to_clean:
+ shutil.rmtree(directory)
+
+ # Remove extra env vars tar-able directory:
+ shutil.rmtree(env_vars_tarpath)
+
+ # Prepare some variables for templating process
+ res_dir = "src/main/res"
+ res_dir_initial = "src/res_initial"
+ # make res_dir stateless
+ if exists(res_dir_initial):
+ pass
+ #shutil.rmtree(res_dir, ignore_errors=True)
+ shutil.copytree(res_dir_initial, res_dir, dirs_exist_ok=True)
+ else:
+ shutil.copytree(res_dir, res_dir_initial)
+
+ # Add user resouces
+ for resource in args.resources:
+ resource_src, resource_dest = resource.split(":")
+ if isfile(realpath(resource_src)):
+ ensure_dir(dirname(join(res_dir, resource_dest)))
+ shutil.copy(realpath(resource_src), join(res_dir, resource_dest))
+ else:
+ shutil.copytree(realpath(resource_src),
+ join(res_dir, resource_dest), dirs_exist_ok=True)
+
+ default_icon = 'templates/kivy-icon.png'
+ default_presplash = 'templates/kivy-presplash.jpg'
+ shutil.copy(
+ args.icon or default_icon,
+ join(res_dir, 'mipmap/icon.png')
+ )
+ if args.icon_fg and args.icon_bg:
+ shutil.copy(args.icon_fg, join(res_dir, 'mipmap/icon_foreground.png'))
+ shutil.copy(args.icon_bg, join(res_dir, 'mipmap/icon_background.png'))
+ with open(join(res_dir, 'mipmap-anydpi-v26/icon.xml'), "w") as fd:
+ fd.write("""<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@mipmap/icon_background"/>
+ <foreground android:drawable="@mipmap/icon_foreground"/>
+</adaptive-icon>
+""")
+ elif args.icon_fg or args.icon_bg:
+ print("WARNING: Received an --icon_fg or an --icon_bg argument, but not both. "
+ "Ignoring.")
+
+ if get_bootstrap_name() != "service_only":
+ lottie_splashscreen = join(res_dir, 'raw/splashscreen.json')
+ if args.presplash_lottie:
+ shutil.copy(
+ 'templates/lottie.xml',
+ join(res_dir, 'layout/lottie.xml')
+ )
+ ensure_dir(join(res_dir, 'raw'))
+ shutil.copy(
+ args.presplash_lottie,
+ join(res_dir, 'raw/splashscreen.json')
+ )
+ else:
+ if exists(lottie_splashscreen):
+ remove(lottie_splashscreen)
+ remove(join(res_dir, 'layout/lottie.xml'))
+
+ shutil.copy(
+ args.presplash or default_presplash,
+ join(res_dir, 'drawable/presplash.jpg')
+ )
+
+ # If extra Java jars were requested, copy them into the libs directory
+ jars = []
+ if args.add_jar:
+ for jarname in args.add_jar:
+ if not exists(jarname):
+ print('Requested jar does not exist: {}'.format(jarname))
+ sys.exit(-1)
+ shutil.copy(jarname, 'src/main/libs')
+ jars.append(basename(jarname))
+
+ # If extra aar were requested, copy them into the libs directory
+ aars = []
+ if args.add_aar:
+ ensure_dir("libs")
+ for aarname in args.add_aar:
+ if not exists(aarname):
+ print('Requested aar does not exists: {}'.format(aarname))
+ sys.exit(-1)
+ shutil.copy(aarname, 'libs')
+ aars.append(basename(aarname).rsplit('.', 1)[0])
+
+ versioned_name = (args.name.replace(' ', '').replace('\'', '') +
+ '-' + args.version)
+
+ version_code = 0
+ if not args.numeric_version:
+ """
+ Set version code in format (10 + minsdk + app_version)
+ Historically versioning was (arch + minsdk + app_version),
+ with arch expressed with a single digit from 6 to 9.
+ Since the multi-arch support, has been changed to 10.
+ """
+ min_sdk = args.min_sdk_version
+ for i in args.version.split('.'):
+ version_code *= 100
+ version_code += int(i)
+ args.numeric_version = "{}{}{}".format("10", min_sdk, version_code)
+
+ if args.intent_filters:
+ with open(args.intent_filters) as fd:
+ args.intent_filters = fd.read()
+
+ if not args.add_activity:
+ args.add_activity = []
+
+ if not args.activity_launch_mode:
+ args.activity_launch_mode = ''
+
+ if args.extra_source_dirs:
+ esd = []
+ for spec in args.extra_source_dirs:
+ if ':' in spec:
+ specdir, specincludes = spec.split(':')
+ print('WARNING: Currently gradle builds only support including source '
+ 'directories, so when building using gradle all files in '
+ '{} will be included.'.format(specdir))
+ else:
+ specdir = spec
+ specincludes = '**'
+ esd.append((realpath(specdir), specincludes))
+ args.extra_source_dirs = esd
+ else:
+ args.extra_source_dirs = []
+
+ service = False
+ if args.private:
+ service_main = join(realpath(args.private), 'service', 'main.py')
+ if exists(service_main) or exists(service_main + 'o'):
+ service = True
+
+ service_names = []
+ base_service_class = args.service_class_name.split('.')[-1]
+ for sid, spec in enumerate(args.services):
+ spec = spec.split(':')
+ name = spec[0]
+ entrypoint = spec[1]
+ options = spec[2:]
+
+ foreground = 'foreground' in options
+ sticky = 'sticky' in options
+
+ service_names.append(name)
+ service_target_path =\
+ 'src/main/java/{}/Service{}.java'.format(
+ args.package.replace(".", "/"),
+ name.capitalize()
+ )
+ render(
+ 'Service.tmpl.java',
+ service_target_path,
+ name=name,
+ entrypoint=entrypoint,
+ args=args,
+ foreground=foreground,
+ sticky=sticky,
+ service_id=sid + 1,
+ base_service_class=base_service_class,
+ )
+
+ # Find the SDK directory and target API
+ with open('project.properties', 'r') as fileh:
+ target = fileh.read().strip()
+ android_api = target.split('-')[1]
+
+ if android_api.isdigit():
+ android_api = int(android_api)
+ else:
+ raise ValueError(
+ "failed to extract the Android API level from " +
+ "build.properties. expected int, got: '" +
+ str(android_api) + "'"
+ )
+
+ with open('local.properties', 'r') as fileh:
+ sdk_dir = fileh.read().strip()
+ sdk_dir = sdk_dir[8:]
+
+ # Try to build with the newest available build tools
+ ignored = {".DS_Store", ".ds_store"}
+ build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored]
+ build_tools_versions = sorted(build_tools_versions,
+ key=LooseVersion)
+ build_tools_version = build_tools_versions[-1]
+
+ # Folder name for launcher (used by SDL2 bootstrap)
+ url_scheme = 'kivy'
+
+ # Copy backup rules file if specified and update the argument
+ res_xml_dir = join(res_dir, 'xml')
+ if args.backup_rules:
+ ensure_dir(res_xml_dir)
+ shutil.copy(join(args.private, args.backup_rules), res_xml_dir)
+ args.backup_rules = split(args.backup_rules)[1][:-4]
+
+ # Copy res_xml files to src/main/res/xml
+ if args.res_xmls:
+ ensure_dir(res_xml_dir)
+ for xmlpath in args.res_xmls:
+ if not os.path.exists(xmlpath):
+ xmlpath = join(args.private, xmlpath)
+ shutil.copy(xmlpath, res_xml_dir)
+
+ # Render out android manifest:
+ manifest_path = "src/main/AndroidManifest.xml"
+ render_args = {
+ "args": args,
+ "service": service,
+ "service_names": service_names,
+ "android_api": android_api,
+ "debug": "debug" in args.build_mode,
+ "native_services": args.native_services
+ }
+ if get_bootstrap_name() == "sdl2":
+ render_args["url_scheme"] = url_scheme
+ render(
+ 'AndroidManifest.tmpl.xml',
+ manifest_path,
+ **render_args)
+
+ # Copy the AndroidManifest.xml to the dist root dir so that ant
+ # can also use it
+ if exists('AndroidManifest.xml'):
+ remove('AndroidManifest.xml')
+ shutil.copy(manifest_path, 'AndroidManifest.xml')
+
+ # gradle build templates
+ render(
+ 'build.tmpl.gradle',
+ 'build.gradle',
+ args=args,
+ aars=aars,
+ jars=jars,
+ android_api=android_api,
+ build_tools_version=build_tools_version,
+ debug_build="debug" in args.build_mode,
+ is_library=(get_bootstrap_name() == 'service_library'),
+ )
+
+ # gradle properties
+ render(
+ 'gradle.tmpl.properties',
+ 'gradle.properties',
+ args=args)
+
+ # ant build templates
+ render(
+ 'build.tmpl.xml',
+ 'build.xml',
+ args=args,
+ versioned_name=versioned_name)
+
+ # String resources:
+ timestamp = time.time()
+ if 'SOURCE_DATE_EPOCH' in environ:
+ # for reproducible builds
+ timestamp = int(environ['SOURCE_DATE_EPOCH'])
+ private_version = "{} {} {}".format(
+ args.version,
+ args.numeric_version,
+ timestamp
+ )
+ render_args = {
+ "args": args,
+ "private_version": hashlib.sha1(private_version.encode()).hexdigest()
+ }
+ if get_bootstrap_name() == "sdl2":
+ render_args["url_scheme"] = url_scheme
+ render(
+ 'strings.tmpl.xml',
+ join(res_dir, 'values/strings.xml'),
+ **render_args)
+
+ if exists(join("templates", "custom_rules.tmpl.xml")):
+ render(
+ 'custom_rules.tmpl.xml',
+ 'custom_rules.xml',
+ args=args)
+
+ if get_bootstrap_name() == "webview":
+ render('WebViewLoader.tmpl.java',
+ 'src/main/java/org/kivy/android/WebViewLoader.java',
+ args=args)
+
+ if args.sign:
+ render('build.properties', 'build.properties')
+ else:
+ if exists('build.properties'):
+ os.remove('build.properties')
+
+ # Apply java source patches if any are present:
+ if exists(join('src', 'patches')):
+ print("Applying Java source code patches...")
+ for patch_name in os.listdir(join('src', 'patches')):
+ patch_path = join('src', 'patches', patch_name)
+ print("Applying patch: " + str(patch_path))
+
+ # -N: insist this is FORWARD patch, don't reverse apply
+ # -p1: strip first path component
+ # -t: batch mode, don't ask questions
+ patch_command = ["patch", "-N", "-p1", "-t", "-i", patch_path]
+
+ try:
+ # Use a dry run to establish whether the patch is already applied.
+ # If we don't check this, the patch may be partially applied (which is bad!)
+ subprocess.check_output(patch_command + ["--dry-run"])
+ except subprocess.CalledProcessError as e:
+ if e.returncode == 1:
+ # Return code 1 means not all hunks could be applied, this usually
+ # means the patch is already applied.
+ print("Warning: failed to apply patch (exit code 1), "
+ "assuming it is already applied: ",
+ str(patch_path))
+ else:
+ raise e
+ else:
+ # The dry run worked, so do the real thing
+ subprocess.check_output(patch_command)
+
+
+def parse_permissions(args_permissions):
+ if args_permissions and isinstance(args_permissions[0], list):
+ args_permissions = [p for perm in args_permissions for p in perm]
+
+ def _is_advanced_permission(permission):
+ return permission.startswith("(") and permission.endswith(")")
+
+ def _decode_advanced_permission(permission):
+ SUPPORTED_PERMISSION_PROPERTIES = ["name", "maxSdkVersion", "usesPermissionFlags"]
+ _permission_args = permission[1:-1].split(";")
+ _permission_args = (arg.split("=") for arg in _permission_args)
+ advanced_permission = dict(_permission_args)
+
+ if "name" not in advanced_permission:
+ raise ValueError("Advanced permission must have a name property")
+
+ for key in advanced_permission.keys():
+ if key not in SUPPORTED_PERMISSION_PROPERTIES:
+ raise ValueError(
+ f"Property '{key}' is not supported. "
+ "Advanced permission only supports: "
+ f"{', '.join(SUPPORTED_PERMISSION_PROPERTIES)} properties"
+ )
+
+ return advanced_permission
+
+ _permissions = []
+ for permission in args_permissions:
+ if _is_advanced_permission(permission):
+ _permissions.append(_decode_advanced_permission(permission))
+ else:
+ if "." in permission:
+ _permissions.append(dict(name=permission))
+ else:
+ _permissions.append(dict(name=f"android.permission.{permission}"))
+ return _permissions
+
+
+def get_sdl_orientation_hint(orientations):
+ SDL_ORIENTATION_MAP = {
+ "landscape": "LandscapeLeft",
+ "portrait": "Portrait",
+ "portrait-reverse": "PortraitUpsideDown",
+ "landscape-reverse": "LandscapeRight",
+ }
+ return " ".join(
+ [SDL_ORIENTATION_MAP[x] for x in orientations if x in SDL_ORIENTATION_MAP]
+ )
+
+
+def get_manifest_orientation(orientations, manifest_orientation=None):
+ # If the user has specifically set an orientation to use in the manifest,
+ # use that.
+ if manifest_orientation is not None:
+ return manifest_orientation
+
+ # If multiple or no orientations are specified, use unspecified in the manifest,
+ # as we can only specify one orientation in the manifest.
+ if len(orientations) != 1:
+ return "unspecified"
+
+ # Convert the orientation to a value that can be used in the manifest.
+ # If the specified orientation is not supported, use unspecified.
+ MANIFEST_ORIENTATION_MAP = {
+ "landscape": "landscape",
+ "portrait": "portrait",
+ "portrait-reverse": "reversePortrait",
+ "landscape-reverse": "reverseLandscape",
+ }
+ return MANIFEST_ORIENTATION_MAP.get(orientations[0], "unspecified")
+
+
+def get_dist_ndk_min_api_level():
+ # Get the default minsdk, equal to the NDK API that this dist is built against
+ try:
+ with open('dist_info.json', 'r') as fileh:
+ info = json.load(fileh)
+ ndk_api = int(info['ndk_api'])
+ except (OSError, KeyError, ValueError, TypeError):
+ print('WARNING: Failed to read ndk_api from dist info, defaulting to 12')
+ ndk_api = 12 # The old default before ndk_api was introduced
+ return ndk_api
+
+
+def create_argument_parser():
+ ndk_api = get_dist_ndk_min_api_level()
+ import argparse
+ ap = argparse.ArgumentParser(description='''\
+Package a Python application for Android (using
+bootstrap ''' + get_bootstrap_name() + ''').
+
+For this to work, Java and Ant need to be in your path, as does the
+tools directory of the Android SDK.
+''')
+
+ # --private is required unless for sdl2, where there's also --launcher
+ ap.add_argument('--private', dest='private',
+ help='the directory with the app source code files' +
+ ' (containing your main.py entrypoint)',
+ required=(get_bootstrap_name() != "sdl2"))
+ ap.add_argument('--package', dest='package',
+ help=('The name of the java package the project will be'
+ ' packaged under.'),
+ required=True)
+ ap.add_argument('--name', dest='name',
+ help=('The human-readable name of the project.'),
+ required=True)
+ ap.add_argument('--numeric-version', dest='numeric_version',
+ help=('The numeric version number of the project. If not '
+ 'given, this is automatically computed from the '
+ 'version.'))
+ ap.add_argument('--version', dest='version',
+ help=('The version number of the project. This should '
+ 'consist of numbers and dots, and should have the '
+ 'same number of groups of numbers as previous '
+ 'versions.'),
+ required=True)
+ if get_bootstrap_name() == "sdl2":
+ ap.add_argument('--launcher', dest='launcher', action='store_true',
+ help=('Provide this argument to build a multi-app '
+ 'launcher, rather than a single app.'))
+ ap.add_argument('--home-app', dest='home_app', action='store_true', default=False,
+ help=('Turn your application into a home app (launcher)'))
+ ap.add_argument('--permission', dest='permissions', action='append', default=[],
+ help='The permissions to give this app.', nargs='+')
+ ap.add_argument('--meta-data', dest='meta_data', action='append', default=[],
+ help='Custom key=value to add in application metadata')
+ ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[],
+ help='Used shared libraries included using <uses-library> tag in AndroidManifest.xml')
+ ap.add_argument('--asset', dest='assets',
+ action="append", default=[],
+ metavar="/path/to/source:dest",
+ help='Put this in the assets folder at assets/dest')
+ ap.add_argument('--resource', dest='resources',
+ action="append", default=[],
+ metavar="/path/to/source:kind/asset",
+ help='Put this in the res folder at res/kind')
+ ap.add_argument('--icon', dest='icon',
+ help=('A png file to use as the icon for '
+ 'the application.'))
+ ap.add_argument('--icon-fg', dest='icon_fg',
+ help=('A png file to use as the foreground of the adaptive icon '
+ 'for the application.'))
+ ap.add_argument('--icon-bg', dest='icon_bg',
+ help=('A png file to use as the background of the adaptive icon '
+ 'for the application.'))
+ ap.add_argument('--service', dest='services', action='append', default=[],
+ help='Declare a new service entrypoint: '
+ 'NAME:PATH_TO_PY[:foreground]')
+ ap.add_argument('--native-service', dest='native_services', action='append', default=[],
+ help='Declare a new native service: '
+ 'package.name.service')
+ if get_bootstrap_name() != "service_only":
+ ap.add_argument('--presplash', dest='presplash',
+ help=('A jpeg file to use as a screen while the '
+ 'application is loading.'))
+ ap.add_argument('--presplash-lottie', dest='presplash_lottie',
+ help=('A lottie (json) file to use as an animation while the '
+ 'application is loading.'))
+ ap.add_argument('--presplash-color',
+ dest='presplash_color',
+ default='#000000',
+ help=('A string to set the loading screen '
+ 'background color. '
+ 'Supported formats are: '
+ '#RRGGBB #AARRGGBB or color names '
+ 'like red, green, blue, etc.'))
+ ap.add_argument('--window', dest='window', action='store_true',
+ default=False,
+ help='Indicate if the application will be windowed')
+ ap.add_argument('--manifest-orientation', dest='manifest_orientation',
+ help=('The orientation that will be set in the '
+ 'android:screenOrientation attribute of the activity '
+ 'in the AndroidManifest.xml file. If not set, '
+ 'the value will be synthesized from the --orientation option.'))
+ ap.add_argument('--orientation', dest='orientation',
+ action="append", default=[],
+ choices=['portrait', 'landscape', 'landscape-reverse', 'portrait-reverse'],
+ help=('The orientations that the app will display in. '
+ 'Since Android ignores android:screenOrientation '
+ 'when in multi-window mode (Which is the default on Android 12+), '
+ 'this option will also set the window orientation hints '
+ 'for apps using the (default) SDL bootstrap.'
+ 'If multiple orientations are given, android:screenOrientation '
+ 'will be set to "unspecified"'))
+
+ ap.add_argument('--enable-androidx', dest='enable_androidx',
+ action='store_true',
+ help=('Enable the AndroidX support library, '
+ 'requires api = 28 or greater'))
+ ap.add_argument('--android-entrypoint', dest='android_entrypoint',
+ default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS,
+ help='Defines which java class will be used for startup, usually a subclass of PythonActivity')
+ ap.add_argument('--android-apptheme', dest='android_apptheme',
+ default='@android:style/Theme.NoTitleBar',
+ help='Defines which app theme should be selected for the main activity')
+ ap.add_argument('--add-compile-option', dest='compile_options', default=[],
+ action='append', help='add compile options to gradle.build')
+ ap.add_argument('--add-gradle-repository', dest='gradle_repositories',
+ default=[],
+ action='append',
+ help='Ddd a repository for gradle')
+ ap.add_argument('--add-packaging-option', dest='packaging_options',
+ default=[],
+ action='append',
+ help='Dndroid packaging options')
+
+ ap.add_argument('--wakelock', dest='wakelock', action='store_true',
+ help=('Indicate if the application needs the device '
+ 'to stay on'))
+ ap.add_argument('--blacklist', dest='blacklist',
+ default=join(curdir, 'blacklist.txt'),
+ help=('Use a blacklist file to match unwanted file in '
+ 'the final APK'))
+ ap.add_argument('--whitelist', dest='whitelist',
+ default=join(curdir, 'whitelist.txt'),
+ help=('Use a whitelist file to prevent blacklisting of '
+ 'file in the final APK'))
+ ap.add_argument('--release', dest='build_mode', action='store_const',
+ const='release', default='debug',
+ help='Build your app as a non-debug release build. '
+ '(Disables gdb debugging among other things)')
+ ap.add_argument('--with-debug-symbols', dest='with_debug_symbols',
+ action='store_const', const=True, default=False,
+ help='Will keep debug symbols from `.so` files.')
+ ap.add_argument('--add-jar', dest='add_jar', action='append',
+ help=('Add a Java .jar to the libs, so you can access its '
+ 'classes with pyjnius. You can specify this '
+ 'argument more than once to include multiple jars'))
+ ap.add_argument('--add-aar', dest='add_aar', action='append',
+ help=('Add an aar dependency manually'))
+ ap.add_argument('--depend', dest='depends', action='append',
+ help=('Add a external dependency '
+ '(eg: com.android.support:appcompat-v7:19.0.1)'))
+ # The --sdk option has been removed, it is ignored in favour of
+ # --android-api handled by toolchain.py
+ ap.add_argument('--sdk', dest='sdk_version', default=-1,
+ type=int, help=('Deprecated argument, does nothing'))
+ ap.add_argument('--minsdk', dest='min_sdk_version',
+ default=ndk_api, type=int,
+ help=('Minimum Android SDK version that the app supports. '
+ 'Defaults to {}.'.format(ndk_api)))
+ ap.add_argument('--allow-minsdk-ndkapi-mismatch', default=False,
+ action='store_true',
+ help=('Allow the --minsdk argument to be different from '
+ 'the discovered ndk_api in the dist'))
+ ap.add_argument('--intent-filters', dest='intent_filters',
+ help=('Add intent-filters xml rules to the '
+ 'AndroidManifest.xml file. The argument is a '
+ 'filename containing xml. The filename should be '
+ 'located relative to the python-for-android '
+ 'directory'))
+ ap.add_argument('--res_xml', dest='res_xmls', action='append', default=[],
+ help='Add files to res/xml directory (for example device-filters)', nargs='+')
+ ap.add_argument('--with-billing', dest='billing_pubkey',
+ help='If set, the billing service will be added (not implemented)')
+ ap.add_argument('--add-source', dest='extra_source_dirs', action='append',
+ help='Include additional source dirs in Java build')
+ if get_bootstrap_name() == "webview":
+ ap.add_argument('--port',
+ help='The port on localhost that the WebView will access',
+ default='5000')
+ ap.add_argument('--try-system-python-compile', dest='try_system_python_compile',
+ action='store_true',
+ help='Use the system python during compileall if possible.')
+ ap.add_argument('--sign', action='store_true',
+ help=('Try to sign the APK with your credentials. You must set '
+ 'the appropriate environment variables.'))
+ ap.add_argument('--add-activity', dest='add_activity', action='append',
+ help='Add this Java class as an Activity to the manifest.')
+ ap.add_argument('--activity-launch-mode',
+ dest='activity_launch_mode',
+ default='singleTask',
+ help='Set the launch mode of the main activity in the manifest.')
+ ap.add_argument('--allow-backup', dest='allow_backup', default='true',
+ help="if set to 'false', then android won't backup the application.")
+ ap.add_argument('--backup-rules', dest='backup_rules', default='',
+ help=('Backup rules for Android Auto Backup. Argument is a '
+ 'filename containing xml. The filename should be '
+ 'located relative to the private directory containing your source code '
+ 'files (containing your main.py entrypoint). '
+ 'See https://developer.android.com/guide/topics/data/'
+ 'autobackup#IncludingFiles for more information'))
+ ap.add_argument('--no-byte-compile-python', dest='byte_compile_python',
+ action='store_false', default=True,
+ help='Skip byte compile for .py files.')
+ ap.add_argument('--no-optimize-python', dest='optimize_python',
+ action='store_false', default=True,
+ help=('Whether to compile to optimised .pyc files, using -OO '
+ '(strips docstrings and asserts)'))
+ ap.add_argument('--extra-manifest-xml', default='',
+ help=('Extra xml to write directly inside the <manifest> element of'
+ 'AndroidManifest.xml'))
+ ap.add_argument('--extra-manifest-application-arguments', default='',
+ help='Extra arguments to be added to the <manifest><application> tag of'
+ 'AndroidManifest.xml')
+ ap.add_argument('--manifest-placeholders', dest='manifest_placeholders',
+ default='[:]', help=('Inject build variables into the manifest '
+ 'via the manifestPlaceholders property'))
+ ap.add_argument('--service-class-name', dest='service_class_name', default=DEFAULT_PYTHON_SERVICE_JAVA_CLASS,
+ help='Use that parameter if you need to implement your own PythonServive Java class')
+ ap.add_argument('--activity-class-name', dest='activity_class_name', default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS,
+ help='The full java class name of the main activity')
+
+ return ap
+
+
+def parse_args_and_make_package(args=None):
+ global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
+
+ ndk_api = get_dist_ndk_min_api_level()
+ ap = create_argument_parser()
+
+ # Put together arguments, and add those from .p4a config file:
+ if args is None:
+ args = sys.argv[1:]
+
+ def _read_configuration():
+ if not exists(".p4a"):
+ return
+ print("Reading .p4a configuration")
+ with open(".p4a") as fd:
+ lines = fd.readlines()
+ lines = [shlex.split(line)
+ for line in lines if not line.startswith("#")]
+ for line in lines:
+ for arg in line:
+ args.append(arg)
+ _read_configuration()
+
+ args = ap.parse_args(args)
+
+ if args.name and args.name[0] == '"' and args.name[-1] == '"':
+ args.name = args.name[1:-1]
+
+ if ndk_api != args.min_sdk_version:
+ print(('WARNING: --minsdk argument does not match the api that is '
+ 'compiled against. Only proceed if you know what you are '
+ 'doing, otherwise use --minsdk={} or recompile against api '
+ '{}').format(ndk_api, args.min_sdk_version))
+ if not args.allow_minsdk_ndkapi_mismatch:
+ print('You must pass --allow-minsdk-ndkapi-mismatch to build '
+ 'with --minsdk different to the target NDK api from the '
+ 'build step')
+ sys.exit(1)
+ else:
+ print('Proceeding with --minsdk not matching build target api')
+
+ if args.billing_pubkey:
+ print('Billing not yet supported!')
+ sys.exit(1)
+
+ if args.sdk_version == -1:
+ print('WARNING: Received a --sdk argument, but this argument is '
+ 'deprecated and does nothing.')
+ args.sdk_version = -1 # ensure it is not used
+
+ args.permissions = parse_permissions(args.permissions)
+
+ args.manifest_orientation = get_manifest_orientation(
+ args.orientation, args.manifest_orientation
+ )
+
+ if get_bootstrap_name() == "sdl2":
+ args.sdl_orientation_hint = get_sdl_orientation_hint(args.orientation)
+
+ if args.res_xmls and isinstance(args.res_xmls[0], list):
+ args.res_xmls = [x for res in args.res_xmls for x in res]
+
+ if args.try_system_python_compile:
+ # Hardcoding python2.7 is okay for now, as python3 skips the
+ # compilation anyway
+ python_executable = 'python2.7'
+ try:
+ subprocess.call([python_executable, '--version'])
+ except (OSError, subprocess.CalledProcessError):
+ pass
+ else:
+ PYTHON = python_executable
+
+ if args.blacklist:
+ with open(args.blacklist) as fd:
+ patterns = [x.strip() for x in fd.read().splitlines()
+ if x.strip() and not x.strip().startswith('#')]
+ BLACKLIST_PATTERNS += patterns
+
+ if args.whitelist:
+ with open(args.whitelist) as fd:
+ patterns = [x.strip() for x in fd.read().splitlines()
+ if x.strip() and not x.strip().startswith('#')]
+ WHITELIST_PATTERNS += patterns
+
+ if args.private is None and \
+ get_bootstrap_name() == 'sdl2' and args.launcher is None:
+ print('Need --private directory or ' +
+ '--launcher (SDL2 bootstrap only)' +
+ 'to have something to launch inside the .apk!')
+ sys.exit(1)
+ make_package(args)
+
+ return args
+
+
+if __name__ == "__main__":
+ if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'):
+ WHITELIST_PATTERNS.append('pyconfig.h')
+ parse_args_and_make_package()

diff --git a/sbapp/services/sidebandservice.py b/sbapp/services/sidebandservice.py
index 6bacded7..171bf837 100644
--- a/sbapp/services/sidebandservice.py
+++ b/sbapp/services/sidebandservice.py
@@ -1,4 +1,4 @@
-__debug_build__ = False
+__debug_build__ = True
import time
import RNS

diff --git a/sbapp/ui/announces.py b/sbapp/ui/announces.py
index f60abacd..dae71042 100644
--- a/sbapp/ui/announces.py
+++ b/sbapp/ui/announces.py
@@ -222,12 +222,12 @@ class Announces():
item.dmenu = MDDropdownMenu(
caller=item.iconr,
items=dm_items,
- position="center",
- width_mult=4,
- elevation=1,
- radius=dp(3),
- opening_transition="linear",
- opening_time=0.0,
+ #position="center",
+ #width_mult=4,
+ #elevation=1,
+ #radius=dp(3),
+ #opening_transition="linear",
+ #opening_time=0.0,
)
def callback_factory(ref):

diff --git a/sbapp/ui/conversations.py b/sbapp/ui/conversations.py
index 6d158b24..c0768882 100644
--- a/sbapp/ui/conversations.py
+++ b/sbapp/ui/conversations.py
@@ -220,6 +220,8 @@ class Conversations():
item.dmenu.dismiss()
return x
+ item.iconr = IconRightWidget(icon="dots-vertical");
+
if self.conversation_dropdown == None:
dmi_h = 40
dm_items = [
@@ -250,18 +252,21 @@ class Conversations():
]
self.conversation_dropdown = MDDropdownMenu(
- caller=None,
+ caller=item.iconr,
items=dm_items,
position="auto",
- width_mult=4,
- elevation=1,
- radius=dp(3),
- opening_transition="linear",
- opening_time=0.0,
+ #border_margin=dp(24),
+ #width=dp(256),
+ #elevation=0,
+ #radius=dp(3),
+ show_transition="linear",
+ hide_transition="linear",
+ #show_duration=0.1,
+ #hide_duration=0.1,
+
)
self.conversation_dropdown.effect_cls = ScrollEffect
- item.iconr = IconRightWidget(icon="dots-vertical");
item.dmenu = self.conversation_dropdown
def callback_factory(ref, dest):

diff --git a/sbapp/ui/helpers.py b/sbapp/ui/helpers.py
index 68f9c72c..2ef4942a 100644
--- a/sbapp/ui/helpers.py
+++ b/sbapp/ui/helpers.py
@@ -25,7 +25,7 @@ intensity_msgs_light = "500"
class ContentNavigationDrawer(Screen):
pass
-class DrawerList(ThemableBehavior, MDList):
+class DrawerList(MDList):
pass
class IconListItem(OneLineIconListItem):

diff --git a/sbapp/ui/messages.py b/sbapp/ui/messages.py
index 9f93b720..e8b80608 100644
--- a/sbapp/ui/messages.py
+++ b/sbapp/ui/messages.py
@@ -429,12 +429,12 @@ class Messages():
item.dmenu = MDDropdownMenu(
caller=item.ids.msg_submenu,
items=dm_items,
- position="center",
- width_mult=4,
- elevation=1,
- radius=dp(3),
- opening_transition="linear",
- opening_time=0.0,
+ #position="center",
+ #width_mult=4,
+ #elevation=1,
+ #radius=dp(3),
+ #opening_transition="linear",
+ #opening_time=0.0,
)
def callback_factory(ref):

diff --git a/setup.py b/setup.py
index 98f50238..90f5e878 100644
--- a/setup.py
+++ b/setup.py
@@ -81,9 +81,9 @@ setuptools.setup(
'sideband=sbapp:main.run',
]
},
- install_requires=["rns>=0.4.6", "lxmf>=0.2.8", "kivy==2.1.0", "plyer", "pillow", "qrcode"],
+ install_requires=["rns>=0.5.6", "lxmf>=0.3.1", "kivy>=2.2.1", "plyer", "pillow", "qrcode", "materialyoucolor"],
extras_require={
"macos": ["pyobjus"],
},
- python_requires='>=3.6',
+ python_requires='>=3.8',
)


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────